Compare commits
128 Commits
Author | SHA1 | Date | |
---|---|---|---|
|
fdc36b64ef | ||
|
13e1c1bbb4 | ||
|
0f75b1cbef | ||
|
07325dde93 | ||
|
fc2e21cfd6 | ||
|
af27264a03 | ||
|
ed7523c311 | ||
|
fe0c57fc09 | ||
|
971b8f2521 | ||
|
ab82f05a21 | ||
|
f835cdf1d6 | ||
|
ac71701d8c | ||
|
07f1eb855e | ||
|
8a9aa00289 | ||
|
bf58d46748 | ||
|
ab5bce1356 | ||
|
95fc74327d | ||
|
c3e79d839f | ||
|
996061b126 | ||
|
05a7b11ba6 | ||
|
681ccd815c | ||
|
0b55d8980c | ||
|
b2db10d68b | ||
|
fd975db6a5 | ||
|
92f746b23a | ||
|
7bc59a8b27 | ||
|
7cda93aeb3 | ||
|
c84c18cebf | ||
|
d8992e4412 | ||
|
d30f613622 | ||
|
86f32dc7d9 | ||
|
32f37d97d7 | ||
|
b39b137e20 | ||
|
a8a12cafc9 | ||
|
c97c321740 | ||
|
196e3f9fd7 | ||
|
caea507b86 | ||
|
db15477448 | ||
|
ce66287189 | ||
|
fb08bb280c | ||
|
e17be63d46 | ||
|
ff0020b47b | ||
|
b1ba13419d | ||
|
b1e9f5824b | ||
|
225c8e6abd | ||
|
f4dd972e54 | ||
|
7d936f4844 | ||
|
f52deeed03 | ||
|
40f8eb5b1b | ||
|
9c800af52e | ||
|
2ee2a943a7 | ||
|
e8a78c2eaa | ||
|
d747f1db5f | ||
|
ef7089dc61 | ||
|
2014da4ca3 | ||
|
593cd8c12d | ||
|
8dc93c66c4 | ||
|
c5a72342f2 | ||
|
67711ee2c7 | ||
|
6d41b077a9 | ||
|
406c6015c4 | ||
|
d57b44ab51 | ||
|
3ad9d03460 | ||
|
e07002721d | ||
|
a5a012dd96 | ||
|
575a14c394 | ||
|
20dd872fbe | ||
|
5869922efb | ||
|
b88ddd41c3 | ||
|
bfb94377f4 | ||
|
cffd872346 | ||
|
d0f963def3 | ||
|
e34a5ae0f3 | ||
|
cbadfe5128 | ||
|
61b59517fc | ||
|
b801bf699f | ||
|
e34b880a55 | ||
|
ac0ef71d9f | ||
|
9533787aac | ||
|
29eaa11052 | ||
|
9ee285e139 | ||
|
ef50f7c2e4 | ||
|
a592bbc76a | ||
|
9f4380a4a3 | ||
|
3834acfa2b | ||
|
c30bf38438 | ||
|
5f25043b94 | ||
|
c3c531931c | ||
|
32b0dc7d59 | ||
|
04f2be01d9 | ||
|
e5ea79aef8 | ||
|
f8d1b4c8f2 | ||
|
8de4eb7ba1 | ||
|
0507d93b3d | ||
|
7f135438b8 | ||
|
a8fce3cbbc | ||
|
99c4046ebf | ||
|
efbe826a4e | ||
|
416c1a58f6 | ||
|
f8d79d0ecc | ||
|
fddfe80222 | ||
|
876f8e320f | ||
|
93fe6457b3 | ||
|
a34a03e036 | ||
|
68105841c6 | ||
|
1789f1e94c | ||
|
55ac682d36 | ||
|
5f01503df6 | ||
|
ce29a6f436 | ||
|
04ee69ce02 | ||
|
7923e58428 | ||
|
2dc11ce1e3 | ||
|
e421cff225 | ||
|
fbd2facfa1 | ||
|
fbbfa568a8 | ||
|
169bc5c3e7 | ||
|
3c451f06ca | ||
|
ee17db29b6 | ||
|
5573c4dde7 | ||
|
cf1e1dd480 | ||
|
30b160ee55 | ||
|
f86e20be53 | ||
|
ae8cfee616 | ||
|
281f876834 | ||
|
8c1b3676ab | ||
|
dff392e558 | ||
|
876a3308d2 | ||
|
cb95bb6df8 |
14
.github/workflows/go.yml
vendored
14
.github/workflows/go.yml
vendored
@ -16,8 +16,8 @@ jobs:
|
|||||||
- name: Set up Go 1.x
|
- name: Set up Go 1.x
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v2
|
||||||
with:
|
with:
|
||||||
# Run on the latest minor release of Go 1.14:
|
# Run on the latest minor release of Go 1.18:
|
||||||
go-version: ^1.14
|
go-version: ^1.18
|
||||||
id: go
|
id: go
|
||||||
|
|
||||||
- name: Check out code into the Go module directory
|
- name: Check out code into the Go module directory
|
||||||
@ -33,7 +33,7 @@ jobs:
|
|||||||
|
|
||||||
- name: Ensure all files were formatted as per gofmt
|
- name: Ensure all files were formatted as per gofmt
|
||||||
run: |
|
run: |
|
||||||
gofmt -l $(find . -name '*.go') >/dev/null
|
[ "$(gofmt -l $(find . -name '*.go') 2>&1)" = "" ]
|
||||||
|
|
||||||
- name: Go Vet
|
- name: Go Vet
|
||||||
run: |
|
run: |
|
||||||
@ -51,8 +51,8 @@ jobs:
|
|||||||
- name: Set up Go 1.x
|
- name: Set up Go 1.x
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v2
|
||||||
with:
|
with:
|
||||||
# Run on the latest minor release of Go 1.14:
|
# Run on the latest minor release of Go 1.18:
|
||||||
go-version: ^1.14
|
go-version: ^1.18
|
||||||
id: go
|
id: go
|
||||||
|
|
||||||
- name: Check out code into the Go module directory
|
- name: Check out code into the Go module directory
|
||||||
@ -78,8 +78,8 @@ jobs:
|
|||||||
- name: Set up Go 1.x
|
- name: Set up Go 1.x
|
||||||
uses: actions/setup-go@v2
|
uses: actions/setup-go@v2
|
||||||
with:
|
with:
|
||||||
# Run on the latest minor release of Go 1.14:
|
# Run on the latest minor release of Go 1.18:
|
||||||
go-version: ^1.14
|
go-version: ^1.18
|
||||||
id: go
|
id: go
|
||||||
|
|
||||||
- name: Check out code into the Go module directory
|
- name: Check out code into the Go module directory
|
||||||
|
53
Makefile
53
Makefile
@ -1,10 +1,19 @@
|
|||||||
SUDO=GOPATH=$(shell go env GOPATH) sudo --preserve-env=GOPATH
|
SUDO=GOPATH=$(shell go env GOPATH) sudo --preserve-env=GOPATH --preserve-env=PATH --preserve-env=HOME
|
||||||
|
|
||||||
PKGS := github.com/rtr7/router7/cmd/... \
|
PKGS := github.com/gokrazy/breakglass \
|
||||||
github.com/gokrazy/breakglass \
|
|
||||||
github.com/gokrazy/timestamps \
|
github.com/gokrazy/timestamps \
|
||||||
github.com/stapelberg/zkj-nas-tools/wolgw \
|
github.com/gokrazy/gdns \
|
||||||
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/...
|
||||||
|
|
||||||
build:
|
build:
|
||||||
mkdir -p result
|
mkdir -p result
|
||||||
@ -20,41 +29,45 @@ ifndef DIR
|
|||||||
@echo variable DIR unset
|
@echo variable DIR unset
|
||||||
false
|
false
|
||||||
endif
|
endif
|
||||||
go install github.com/gokrazy/tools/cmd/gokr-packer
|
go install github.com/gokrazy/tools/cmd/gokr-packer@latest
|
||||||
GOARCH=amd64 gokr-packer \
|
GOARCH=amd64 gokr-packer \
|
||||||
-gokrazy_pkgs=github.com/gokrazy/gokrazy/cmd/ntp,github.com/gokrazy/gokrazy/cmd/randomd \
|
-gokrazy_pkgs=github.com/gokrazy/gokrazy/cmd/ntp,github.com/gokrazy/gokrazy/cmd/randomd \
|
||||||
-kernel_package=github.com/rtr7/kernel \
|
-kernel_package=github.com/rtr7/kernel \
|
||||||
-firmware_package=github.com/rtr7/kernel \
|
-firmware_package=github.com/rtr7/kernel \
|
||||||
|
-eeprom_package= \
|
||||||
-overwrite_boot=${DIR}/boot.img \
|
-overwrite_boot=${DIR}/boot.img \
|
||||||
-overwrite_root=${DIR}/root.img \
|
-overwrite_root=${DIR}/root.img \
|
||||||
-overwrite_mbr=${DIR}/mbr.img \
|
-overwrite_mbr=${DIR}/mbr.img \
|
||||||
-serial_console=ttyS0,115200n8 \
|
-serial_console=ttyS0,115200 \
|
||||||
-hostname=router7 \
|
-hostname=router7 \
|
||||||
${PKGS}
|
${PKGS}
|
||||||
|
|
||||||
recover: #test
|
recover: #test
|
||||||
go install \
|
go install github.com/gokrazy/tools/cmd/gokr-packer@latest
|
||||||
github.com/gokrazy/tools/cmd/gokr-packer \
|
go install github.com/rtr7/tools/cmd/rtr7-recover@latest
|
||||||
github.com/rtr7/tools/cmd/rtr7-recover
|
|
||||||
GOARCH=amd64 gokr-packer \
|
GOARCH=amd64 gokr-packer \
|
||||||
-gokrazy_pkgs=github.com/gokrazy/gokrazy/cmd/ntp,github.com/gokrazy/gokrazy/cmd/randomd \
|
-gokrazy_pkgs=github.com/gokrazy/gokrazy/cmd/ntp,github.com/gokrazy/gokrazy/cmd/randomd \
|
||||||
-kernel_package=github.com/rtr7/kernel \
|
-kernel_package=github.com/rtr7/kernel \
|
||||||
-firmware_package=github.com/rtr7/kernel \
|
-firmware_package=github.com/rtr7/kernel \
|
||||||
|
-eeprom_package= \
|
||||||
-overwrite_boot=/tmp/recovery/boot.img \
|
-overwrite_boot=/tmp/recovery/boot.img \
|
||||||
-overwrite_root=/tmp/recovery/root.img \
|
-overwrite_root=/tmp/recovery/root.img \
|
||||||
-serial_console=ttyS0,115200n8 \
|
-serial_console=ttyS0,115200n8 \
|
||||||
-hostname=router7 \
|
-hostname=router7 \
|
||||||
${PKGS}
|
${PKGS}
|
||||||
${SUDO} /home/michael/go/bin/rtr7-recover \
|
${SUDO} $(which rtr7-recover) \
|
||||||
-boot=/tmp/recovery/boot.img \
|
--boot /tmp/recovery/boot.img \
|
||||||
-root=/tmp/recovery/root.img
|
--root /tmp/recovery/root.img \
|
||||||
|
--mbr /tmp/recovery/mbr.img \
|
||||||
|
--hostname router7 \
|
||||||
|
--interface enp0s31f6
|
||||||
|
|
||||||
test:
|
test:
|
||||||
# simulate recover (quick, for early for feedback)
|
# simulate recover (quick, for early for feedback)
|
||||||
go build ${PKGS} github.com/rtr7/tools/cmd/...
|
go build -mod=mod ${PKGS} github.com/rtr7/tools/cmd/...
|
||||||
go test -count=1 -v -race github.com/rtr7/router7/internal/...
|
go test -mod=mod -count=1 -v -race github.com/rtr7/router7/internal/...
|
||||||
# integration tests
|
# integration tests
|
||||||
${SUDO} $(shell go env GOROOT)/bin/go test -count=1 -v -race github.com/rtr7/router7/...
|
${SUDO} $(shell go env GOROOT)/bin/go test -buildvcs=false -count=1 -v -race github.com/rtr7/router7/...
|
||||||
|
|
||||||
testdhcp:
|
testdhcp:
|
||||||
go test -v -coverprofile=/tmp/cov github.com/rtr7/router7/internal/dhcp4d
|
go test -v -coverprofile=/tmp/cov github.com/rtr7/router7/internal/dhcp4d
|
||||||
@ -68,20 +81,23 @@ strace:
|
|||||||
(cd /tmp && go test -c router7) && ${SUDO} strace -f -o /tmp/st -s 2048 /tmp/router7.test -test.v #-test.race
|
(cd /tmp && go test -c router7) && ${SUDO} strace -f -o /tmp/st -s 2048 /tmp/router7.test -test.v #-test.race
|
||||||
|
|
||||||
update:
|
update:
|
||||||
rtr7-safe-update -build_command='make -C ~/router7 image DIR=$GOKR_DIR'
|
rtr7-safe-update -build_command='make image DIR=$$GOKR_DIR'
|
||||||
|
|
||||||
# sudo ip link add link enp0s31f6 name macvtap0 type macvtap
|
# sudo ip link add link enp3s0 name macvtap0 type macvtap
|
||||||
# sudo ip link set macvtap0 address 52:55:00:d1:55:03 up
|
# 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?
|
# TODO: use veth pairs for router7’s lan0?
|
||||||
# e.g. get a network namespace to talk through router7
|
# e.g. get a network namespace to talk through router7
|
||||||
# ip link add dev veth1 type veth peer name veth2
|
# ip link add dev veth1 type veth peer name veth2
|
||||||
qemu:
|
qemu:
|
||||||
|
mkdir -p /tmp/router7-qemu
|
||||||
GOARCH=amd64 gokr-packer \
|
GOARCH=amd64 gokr-packer \
|
||||||
-gokrazy_pkgs=github.com/gokrazy/gokrazy/cmd/ntp,github.com/gokrazy/gokrazy/cmd/randomd \
|
-gokrazy_pkgs=github.com/gokrazy/gokrazy/cmd/ntp,github.com/gokrazy/gokrazy/cmd/randomd \
|
||||||
-hostname=qemu-router7 \
|
-hostname=qemu-router7 \
|
||||||
-kernel_package=github.com/rtr7/kernel \
|
-kernel_package=github.com/rtr7/kernel \
|
||||||
-firmware_package=github.com/rtr7/kernel \
|
-firmware_package=github.com/rtr7/kernel \
|
||||||
|
-eeprom_package= \
|
||||||
-overwrite=/tmp/router7-qemu/disk.img \
|
-overwrite=/tmp/router7-qemu/disk.img \
|
||||||
-target_storage_bytes=$$((2*1024*1024*1024)) \
|
-target_storage_bytes=$$((2*1024*1024*1024)) \
|
||||||
-serial_console=ttyS0,115200 \
|
-serial_console=ttyS0,115200 \
|
||||||
@ -95,6 +111,7 @@ qemu:
|
|||||||
-device virtio-net-pci,netdev=uplink,mac=52:55:00:d1:55:03 \
|
-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 virtio-net-pci,id=lan,mac=52:55:00:d1:55:04 \
|
||||||
-device i6300esb,id=watchdog0 -watchdog-action reset \
|
-device i6300esb,id=watchdog0 -watchdog-action reset \
|
||||||
|
-bios /usr/share/edk2-ovmf/x64/OVMF_CODE.fd \
|
||||||
-smp 8 \
|
-smp 8 \
|
||||||
-machine accel=kvm \
|
-machine accel=kvm \
|
||||||
-m 4096 \
|
-m 4096 \
|
||||||
|
163
README.md
163
README.md
@ -1,6 +1,6 @@
|
|||||||
# router7
|
# router7
|
||||||
|
|
||||||
[](https://travis-ci.org/rtr7/router7)
|
[](https://github.com/rtr7/router7/actions/workflows/go.yml)
|
||||||
[](https://godoc.org/github.com/rtr7/router7/cmd)
|
[](https://godoc.org/github.com/rtr7/router7/cmd)
|
||||||
[](https://goreportcard.com/report/github.com/rtr7/router7)
|
[](https://goreportcard.com/report/github.com/rtr7/router7)
|
||||||
|
|
||||||
@ -8,163 +8,4 @@ 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.
|
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.
|
||||||
|
|
||||||
## Motivation
|
**For more details, please see [router7.org](https://router7.org/)**
|
||||||
|
|
||||||
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 {
|
func logic() error {
|
||||||
http.HandleFunc("/backup.tar.gz", func(w http.ResponseWriter, r *http.Request) {
|
http.HandleFunc("/backup.tar.gz", func(w http.ResponseWriter, r *http.Request) {
|
||||||
if err := backup.Archive(w, *perm); err != nil {
|
if err := backup.Archive(w, *perm, flag.Args()); err != nil {
|
||||||
log.Printf("backup.tar.gz: %v", err)
|
log.Printf("backup.tar.gz: %v", err)
|
||||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||||
}
|
}
|
||||||
@ -68,6 +68,7 @@ func logic() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
flag.Parse()
|
||||||
if err := logic(); err != nil {
|
if err := logic(); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
//go:build ignore
|
||||||
// +build ignore
|
// +build ignore
|
||||||
|
|
||||||
// Copyright 2018 Google Inc.
|
// Copyright 2018 Google Inc.
|
||||||
|
@ -17,11 +17,14 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net"
|
"net"
|
||||||
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"path"
|
"path"
|
||||||
@ -34,6 +37,7 @@ import (
|
|||||||
"github.com/google/gopacket/layers"
|
"github.com/google/gopacket/layers"
|
||||||
"github.com/google/renameio"
|
"github.com/google/renameio"
|
||||||
"github.com/jpillora/backoff"
|
"github.com/jpillora/backoff"
|
||||||
|
rtr7dhcp4 "github.com/rtr7/dhcp4"
|
||||||
"github.com/rtr7/router7/internal/dhcp4"
|
"github.com/rtr7/router7/internal/dhcp4"
|
||||||
"github.com/rtr7/router7/internal/netconfig"
|
"github.com/rtr7/router7/internal/netconfig"
|
||||||
"github.com/rtr7/router7/internal/notify"
|
"github.com/rtr7/router7/internal/notify"
|
||||||
@ -48,6 +52,45 @@ var (
|
|||||||
perm = flag.String("perm", "/perm", "path to replace /perm")
|
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 {
|
func logic() error {
|
||||||
leasePath := filepath.Join(*stateDir, "wire/lease.json")
|
leasePath := filepath.Join(*stateDir, "wire/lease.json")
|
||||||
if err := os.MkdirAll(filepath.Dir(leasePath), 0755); err != nil {
|
if err := os.MkdirAll(filepath.Dir(leasePath), 0755); err != nil {
|
||||||
@ -94,14 +137,36 @@ func logic() error {
|
|||||||
Min: 10 * time.Second,
|
Min: 10 * time.Second,
|
||||||
Max: 1 * time.Minute,
|
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() {
|
for c.ObtainOrRenew() {
|
||||||
if err := c.Err(); err != nil {
|
if err := c.Err(); err != nil {
|
||||||
dur := backoff.Duration()
|
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)
|
log.Printf("Temporary error: %v (waiting %v)", err, dur)
|
||||||
time.Sleep(dur)
|
time.Sleep(dur)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
backoff.Reset()
|
backoff.Reset()
|
||||||
|
lastSuccess = time.Now()
|
||||||
log.Printf("lease: %+v", c.Config())
|
log.Printf("lease: %+v", c.Config())
|
||||||
b, err := json.Marshal(c.Config())
|
b, err := json.Marshal(c.Config())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -124,17 +189,45 @@ func logic() error {
|
|||||||
if err := notify.Process(path.Join(path.Dir(os.Args[0]), "/netconfigd"), syscall.SIGUSR1); err != nil {
|
if err := notify.Process(path.Join(path.Dir(os.Args[0]), "/netconfigd"), syscall.SIGUSR1); err != nil {
|
||||||
log.Printf("notifying netconfig: %v", err)
|
log.Printf("notifying netconfig: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
unhealthyCycles := 0
|
||||||
|
for {
|
||||||
select {
|
select {
|
||||||
case <-time.After(time.Until(c.Config().RenewAfter)):
|
case <-time.After(time.Until(c.Config().RenewAfter)):
|
||||||
// fallthrough and renew the DHCP lease
|
// 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:
|
case <-usr2:
|
||||||
log.Printf("SIGUSR2 received, sending DHCPRELEASE")
|
log.Printf("SIGUSR2 received, sending DHCPRELEASE")
|
||||||
if err := c.Release(); err != nil {
|
if err := c.Release(); err != nil {
|
||||||
return err
|
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
|
os.Exit(125) // quit supervision by gokrazy
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return c.Err() // permanent error
|
return c.Err() // permanent error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,6 +35,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
"unicode"
|
||||||
|
|
||||||
"github.com/gokrazy/gokrazy"
|
"github.com/gokrazy/gokrazy"
|
||||||
"github.com/google/renameio"
|
"github.com/google/renameio"
|
||||||
@ -46,6 +47,7 @@ import (
|
|||||||
|
|
||||||
"github.com/rtr7/router7/internal/dhcp4d"
|
"github.com/rtr7/router7/internal/dhcp4d"
|
||||||
"github.com/rtr7/router7/internal/multilisten"
|
"github.com/rtr7/router7/internal/multilisten"
|
||||||
|
"github.com/rtr7/router7/internal/netconfig"
|
||||||
"github.com/rtr7/router7/internal/notify"
|
"github.com/rtr7/router7/internal/notify"
|
||||||
"github.com/rtr7/router7/internal/oui"
|
"github.com/rtr7/router7/internal/oui"
|
||||||
"github.com/rtr7/router7/internal/teelogger"
|
"github.com/rtr7/router7/internal/teelogger"
|
||||||
@ -60,6 +62,7 @@ var (
|
|||||||
|
|
||||||
iface = flag.String("interface", "lan0", "ethernet interface to listen for DHCPv4 requests on")
|
iface = flag.String("interface", "lan0", "ethernet interface to listen for DHCPv4 requests on")
|
||||||
perm = flag.String("perm", "/perm", "path to replace /perm")
|
perm = flag.String("perm", "/perm", "path to replace /perm")
|
||||||
|
domain = flag.String("domain", "lan", "domain name for your network")
|
||||||
)
|
)
|
||||||
|
|
||||||
func updateNonExpired(leases []*dhcp4d.Lease) {
|
func updateNonExpired(leases []*dhcp4d.Lease) {
|
||||||
@ -94,6 +97,9 @@ var (
|
|||||||
}
|
}
|
||||||
return dur.Truncate(1 * time.Second).String()
|
return dur.Truncate(1 * time.Second).String()
|
||||||
},
|
},
|
||||||
|
"zero": func(t time.Time) bool {
|
||||||
|
return t.IsZero()
|
||||||
|
},
|
||||||
}).Parse(`<!DOCTYPE html>
|
}).Parse(`<!DOCTYPE html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
@ -152,7 +158,9 @@ form {
|
|||||||
<th>Hostname</th>
|
<th>Hostname</th>
|
||||||
<th>MAC address</th>
|
<th>MAC address</th>
|
||||||
<th>Vendor</th>
|
<th>Vendor</th>
|
||||||
|
<th>VendorIdentifier</th>
|
||||||
<th>Expiry</th>
|
<th>Expiry</th>
|
||||||
|
<th>Last ACK</th>
|
||||||
</tr>
|
</tr>
|
||||||
{{ range $idx, $l := . }}
|
{{ range $idx, $l := . }}
|
||||||
<tr>
|
<tr>
|
||||||
@ -168,27 +176,33 @@ form {
|
|||||||
</td>
|
</td>
|
||||||
<td class="hwaddr">{{$l.HardwareAddr}}</td>
|
<td class="hwaddr">{{$l.HardwareAddr}}</td>
|
||||||
<td>{{$l.Vendor}}</td>
|
<td>{{$l.Vendor}}</td>
|
||||||
|
<td>{{$l.VendorIdentifier}}</td>
|
||||||
<td title="{{ timefmt $l.Expiry }}">
|
<td title="{{ timefmt $l.Expiry }}">
|
||||||
{{ if $l.Expired }}
|
{{ if (not (zero $l.LastACK)) }}
|
||||||
{{ since $l.Expiry }}
|
{{ timefmt $l.LastACK }}
|
||||||
<span class="expired">expired</span>
|
{{ if $l.Active }}
|
||||||
{{ else }}
|
|
||||||
{{ if $l.Static }}
|
|
||||||
<span class="static">static</span>
|
|
||||||
{{ else }}
|
|
||||||
{{ timefmt $l.Expiry }}
|
|
||||||
<span class="active">active</span>
|
<span class="active">active</span>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
{{ if $l.Expired }}
|
||||||
|
<span class="expired">expired</span>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
{{ end }}
|
||||||
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{{ end }}
|
{{ end }}
|
||||||
{{ end }}
|
{{ end }}
|
||||||
|
|
||||||
|
<h1>Static Leases</h1>
|
||||||
<table cellpadding="0" cellspacing="0">
|
<table cellpadding="0" cellspacing="0">
|
||||||
{{ template "table" .StaticLeases }}
|
{{ template "table" .StaticLeases }}
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<h1>Dynamic Leases</h1>
|
||||||
|
<table cellpadding="0" cellspacing="0">
|
||||||
{{ template "table" .DynamicLeases }}
|
{{ template "table" .DynamicLeases }}
|
||||||
</table>
|
</table>
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
`))
|
`))
|
||||||
@ -237,6 +251,8 @@ type srv struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func newSrv(permDir string) (*srv, error) {
|
func newSrv(permDir string) (*srv, error) {
|
||||||
|
mayqtt := MQTT()
|
||||||
|
|
||||||
http.Handle("/metrics", promhttp.Handler())
|
http.Handle("/metrics", promhttp.Handler())
|
||||||
if err := updateListeners(); err != nil {
|
if err := updateListeners(); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -259,7 +275,27 @@ func newSrv(permDir string) (*srv, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
handler, err := dhcp4d.NewHandler(permDir, ifc, *iface, nil)
|
|
||||||
|
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)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@ -282,7 +318,10 @@ func newSrv(permDir string) (*srv, error) {
|
|||||||
http.Error(w, "missing hostname parameter", http.StatusBadRequest)
|
http.Error(w, "missing hostname parameter", http.StatusBadRequest)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
handler.SetHostname(hwaddr, hostname)
|
if err := handler.SetHostname(hwaddr, hostname); err != nil {
|
||||||
|
http.Error(w, err.Error(), http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
http.Redirect(w, r, "/", http.StatusFound)
|
http.Redirect(w, r, "/", http.StatusFound)
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -338,17 +377,20 @@ func newSrv(permDir string) (*srv, error) {
|
|||||||
Vendor string
|
Vendor string
|
||||||
Expired bool
|
Expired bool
|
||||||
Static bool
|
Static bool
|
||||||
|
Active bool
|
||||||
}
|
}
|
||||||
|
|
||||||
leasesMu.Lock()
|
leasesMu.Lock()
|
||||||
defer leasesMu.Unlock()
|
defer leasesMu.Unlock()
|
||||||
static := make([]tmplLease, 0, len(leases))
|
static := make([]tmplLease, 0, len(leases))
|
||||||
dynamic := make([]tmplLease, 0, len(leases))
|
dynamic := make([]tmplLease, 0, len(leases))
|
||||||
|
now := time.Now()
|
||||||
tl := func(l *dhcp4d.Lease) tmplLease {
|
tl := func(l *dhcp4d.Lease) tmplLease {
|
||||||
return tmplLease{
|
return tmplLease{
|
||||||
Lease: *l,
|
Lease: *l,
|
||||||
Vendor: ouiDB.Lookup(l.HardwareAddr[:8]),
|
Vendor: ouiDB.Lookup(l.HardwareAddr[:8]),
|
||||||
Expired: l.Expired(time.Now()),
|
Expired: l.Expired(now),
|
||||||
|
Active: l.Active(now),
|
||||||
Static: l.Expiry.IsZero(),
|
Static: l.Expiry.IsZero(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -399,6 +441,51 @@ func newSrv(permDir string) (*srv, error) {
|
|||||||
if err := notify.Process(path.Join(path.Dir(os.Args[0]), "/dnsd"), syscall.SIGUSR1); err != nil {
|
if err := notify.Process(path.Join(path.Dir(os.Args[0]), "/dnsd"), syscall.SIGUSR1); err != nil {
|
||||||
log.Printf("notifying dnsd: %v", err)
|
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")
|
conn, err := conn.NewUDP4BoundListener(*iface, ":67")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
70
cmd/dhcp4d/mayqtt.go
Normal file
70
cmd/dhcp4d/mayqtt.go
Normal file
@ -0,0 +1,70 @@
|
|||||||
|
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,6 +28,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/renameio"
|
"github.com/google/renameio"
|
||||||
|
"github.com/jpillora/backoff"
|
||||||
"github.com/rtr7/router7/internal/dhcp6"
|
"github.com/rtr7/router7/internal/dhcp6"
|
||||||
"github.com/rtr7/router7/internal/notify"
|
"github.com/rtr7/router7/internal/notify"
|
||||||
"github.com/rtr7/router7/internal/teelogger"
|
"github.com/rtr7/router7/internal/teelogger"
|
||||||
@ -57,12 +58,21 @@ func logic() error {
|
|||||||
}
|
}
|
||||||
usr2 := make(chan os.Signal, 1)
|
usr2 := make(chan os.Signal, 1)
|
||||||
signal.Notify(usr2, syscall.SIGUSR2)
|
signal.Notify(usr2, syscall.SIGUSR2)
|
||||||
|
backoff := backoff.Backoff{
|
||||||
|
Factor: 2,
|
||||||
|
Jitter: true,
|
||||||
|
Min: 10 * time.Second,
|
||||||
|
Max: 1 * time.Minute,
|
||||||
|
}
|
||||||
|
|
||||||
for c.ObtainOrRenew() {
|
for c.ObtainOrRenew() {
|
||||||
if err := c.Err(); err != nil {
|
if err := c.Err(); err != nil {
|
||||||
log.Printf("Temporary error: %v", err)
|
dur := backoff.Duration()
|
||||||
time.Sleep(10 * time.Second)
|
log.Printf("Temporary error: %v (waiting %v)", err, dur)
|
||||||
|
time.Sleep(dur)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
backoff.Reset()
|
||||||
log.Printf("lease: %+v", c.Config())
|
log.Printf("lease: %+v", c.Config())
|
||||||
b, err := json.Marshal(c.Config())
|
b, err := json.Marshal(c.Config())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
@ -34,12 +34,12 @@ import (
|
|||||||
|
|
||||||
"github.com/rtr7/router7/internal/diag"
|
"github.com/rtr7/router7/internal/diag"
|
||||||
"github.com/rtr7/router7/internal/multilisten"
|
"github.com/rtr7/router7/internal/multilisten"
|
||||||
|
|
||||||
|
_ "net/http/pprof"
|
||||||
)
|
)
|
||||||
|
|
||||||
var httpListeners = multilisten.NewPool()
|
var httpListeners = multilisten.NewPool()
|
||||||
|
|
||||||
var perm = flag.String("perm", "/perm", "path to replace /perm")
|
|
||||||
|
|
||||||
func updateListeners() error {
|
func updateListeners() error {
|
||||||
hosts, err := gokrazy.PrivateInterfaceAddrs()
|
hosts, err := gokrazy.PrivateInterfaceAddrs()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -80,34 +80,53 @@ func firstError(re *diag.EvalResult) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
func logic() error {
|
func graph(uplink string, ipv6 bool) *diag.Monitor {
|
||||||
const (
|
const ip6allrouters = "ff02::2" // no /etc/hosts on gokrazy
|
||||||
uplink = "uplink0" /* enp0s31f6 */
|
graph := diag.Link(uplink).
|
||||||
ip6allrouters = "ff02::2" // no /etc/hosts on gokrazy
|
|
||||||
)
|
|
||||||
m := diag.NewMonitor(diag.Link(uplink).
|
|
||||||
Then(diag.DHCPv4().
|
Then(diag.DHCPv4().
|
||||||
Then(diag.Ping4Gateway().
|
Then(diag.Ping4Gateway().
|
||||||
Then(diag.Ping4("google.ch").
|
Then(diag.TCP4("www.google.com:80"))))
|
||||||
Then(diag.TCP4("www.google.ch:80"))))).
|
|
||||||
|
if ipv6 {
|
||||||
|
graph = graph.
|
||||||
Then(diag.DHCPv6().
|
Then(diag.DHCPv6().
|
||||||
Then(diag.Ping6("lan0", "google.ch"))).
|
Then(diag.TCP6("lan0", "www.google.com:80"))).
|
||||||
Then(diag.RouterAdvertisments(uplink).
|
Then(diag.RouterAdvertisments(uplink).
|
||||||
Then(diag.Ping6Gateway().
|
Then(diag.Ping6Gateway().
|
||||||
Then(diag.Ping6(uplink, "google.ch").
|
Then(diag.TCP6(uplink, "www.google.com:80")))).
|
||||||
Then(diag.TCP6("www.google.ch:80"))))).
|
Then(diag.Ping6("", ip6allrouters+"%"+uplink))
|
||||||
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
|
||||||
var mu sync.Mutex
|
var mu sync.Mutex
|
||||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
re := m.Evaluate()
|
re := mHumanReadable.Evaluate()
|
||||||
mu.Unlock()
|
mu.Unlock()
|
||||||
fmt.Fprintf(w, `<!DOCTYPE html><style type="text/css">ul { list-style-type: none; }</style><ul>`)
|
fmt.Fprintf(w, `<!DOCTYPE html><style type="text/css">ul { list-style-type: none; }</style><ul>`)
|
||||||
dump(0, w, re)
|
dump(0, w, re)
|
||||||
})
|
})
|
||||||
http.HandleFunc("/health.json", func(w http.ResponseWriter, r *http.Request) {
|
http.HandleFunc("/health.json", func(w http.ResponseWriter, r *http.Request) {
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
re := m.Evaluate()
|
re := mJSON.Evaluate()
|
||||||
mu.Unlock()
|
mu.Unlock()
|
||||||
reply := struct {
|
reply := struct {
|
||||||
FirstError string `json:"first_error"`
|
FirstError string `json:"first_error"`
|
||||||
@ -135,9 +154,6 @@ func logic() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
flag.Parse()
|
|
||||||
diag.Perm = *perm
|
|
||||||
|
|
||||||
if err := logic(); err != nil {
|
if err := logic(); err != nil {
|
||||||
log.Fatal(err)
|
log.Fatal(err)
|
||||||
}
|
}
|
||||||
|
@ -40,19 +40,20 @@ import (
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
httpListeners = multilisten.NewPool()
|
httpListeners = multilisten.NewPool()
|
||||||
dnsListeners = multilisten.NewPool()
|
dnsUDPListeners = multilisten.NewPool()
|
||||||
|
dnsTCPListeners = multilisten.NewPool()
|
||||||
|
|
||||||
perm = flag.String("perm", "/perm", "path to replace /perm")
|
perm = flag.String("perm", "/perm", "path to replace /perm")
|
||||||
domain = flag.String("domain", "lan", "domain name for your network")
|
domain = flag.String("domain", "lan", "domain name for your network")
|
||||||
)
|
)
|
||||||
|
|
||||||
func updateListeners(mux *miekgdns.ServeMux) error {
|
func updateListeners(mux *miekgdns.ServeMux) error {
|
||||||
hosts, err := gokrazy.PrivateInterfaceAddrs()
|
privateAddrs, err := gokrazy.PrivateInterfaceAddrs()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
dnsListeners.ListenAndServe(hosts, func(host string) multilisten.Listener {
|
dnsUDPListeners.ListenAndServe(privateAddrs, func(host string) multilisten.Listener {
|
||||||
return &listenerAdapter{&miekgdns.Server{
|
return &listenerAdapter{&miekgdns.Server{
|
||||||
Addr: net.JoinHostPort(host, "53"),
|
Addr: net.JoinHostPort(host, "53"),
|
||||||
Net: "udp",
|
Net: "udp",
|
||||||
@ -60,11 +61,19 @@ func updateListeners(mux *miekgdns.ServeMux) error {
|
|||||||
}}
|
}}
|
||||||
})
|
})
|
||||||
|
|
||||||
if net1, err := multilisten.IPv6Net1(*perm); err == nil {
|
dnsTCPListeners.ListenAndServe(privateAddrs, func(host string) multilisten.Listener {
|
||||||
hosts = append(hosts, net1)
|
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)
|
||||||
}
|
}
|
||||||
|
|
||||||
httpListeners.ListenAndServe(hosts, func(host string) multilisten.Listener {
|
httpListeners.ListenAndServe(privateAddrs, func(host string) multilisten.Listener {
|
||||||
return &http.Server{Addr: net.JoinHostPort(host, "8053")}
|
return &http.Server{Addr: net.JoinHostPort(host, "8053")}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -47,6 +47,8 @@ var (
|
|||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
var c nftables.Conn
|
var c nftables.Conn
|
||||||
|
filter4 := &nftables.Table{Family: nftables.TableFamilyIPv4, Name: "filter"}
|
||||||
|
filter6 := &nftables.Table{Family: nftables.TableFamilyIPv6, Name: "filter"}
|
||||||
for _, metric := range []struct {
|
for _, metric := range []struct {
|
||||||
name string
|
name string
|
||||||
labels prometheus.Labels
|
labels prometheus.Labels
|
||||||
@ -56,18 +58,34 @@ func init() {
|
|||||||
{
|
{
|
||||||
name: "filter_forward",
|
name: "filter_forward",
|
||||||
labels: prometheus.Labels{"family": "ipv4"},
|
labels: prometheus.Labels{"family": "ipv4"},
|
||||||
obj: &nftables.CounterObj{
|
obj: &nftables.CounterObj{Table: filter4, Name: "fwded"},
|
||||||
Table: &nftables.Table{Family: nftables.TableFamilyIPv4, Name: "filter"},
|
|
||||||
Name: "fwded",
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "filter_forward",
|
name: "filter_forward",
|
||||||
labels: prometheus.Labels{"family": "ipv6"},
|
labels: prometheus.Labels{"family": "ipv6"},
|
||||||
obj: &nftables.CounterObj{
|
obj: &nftables.CounterObj{Table: filter6, Name: "fwded"},
|
||||||
Table: &nftables.Table{Family: nftables.TableFamilyIPv6, Name: "filter"},
|
|
||||||
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"},
|
||||||
},
|
},
|
||||||
} {
|
} {
|
||||||
metric := metric // copy
|
metric := metric // copy
|
||||||
@ -75,12 +93,11 @@ func init() {
|
|||||||
updateCounter := func() {
|
updateCounter := func() {
|
||||||
mu.Lock()
|
mu.Lock()
|
||||||
defer mu.Unlock()
|
defer mu.Unlock()
|
||||||
objs, err := c.GetObjReset(metric.obj)
|
obj, err := c.ResetObject(metric.obj)
|
||||||
if err != nil ||
|
if err != nil {
|
||||||
len(objs) != 1 {
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if co, ok := objs[0].(*nftables.CounterObj); ok {
|
if co, ok := obj.(*nftables.CounterObj); ok {
|
||||||
metric.packets += co.Packets
|
metric.packets += co.Packets
|
||||||
metric.bytes += co.Bytes
|
metric.bytes += co.Bytes
|
||||||
}
|
}
|
||||||
|
1
docs/CNAME
Normal file
1
docs/CNAME
Normal file
@ -0,0 +1 @@
|
|||||||
|
router7.org
|
221
docs/architecture/index.html
Normal file
221
docs/architecture/index.html
Normal file
@ -0,0 +1,221 @@
|
|||||||
|
<!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
Normal file
7
docs/bootstrap-4.4.1.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
7
docs/bootstrap-4.4.1.min.js
vendored
Normal file
7
docs/bootstrap-4.4.1.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
94
docs/index.html
Normal file
94
docs/index.html
Normal file
@ -0,0 +1,94 @@
|
|||||||
|
<!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>
|
212
docs/installation/index.html
Normal file
212
docs/installation/index.html
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
<!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
Normal file
2
docs/jquery-3.4.1.slim.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
5
docs/popper-1.16.0.min.js
vendored
Normal file
5
docs/popper-1.16.0.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2
docs/robots.txt
Normal file
2
docs/robots.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
User-Agent: *
|
||||||
|
sitemap: https://router7.org/sitemap.xml
|
19
docs/sass/sidebar.css
Normal file
19
docs/sass/sidebar.css
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
.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; }
|
11
docs/sitemap.xml
Normal file
11
docs/sitemap.xml
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
<?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,39 +1,63 @@
|
|||||||
module github.com/rtr7/router7
|
module github.com/rtr7/router7
|
||||||
|
|
||||||
go 1.13
|
go 1.21
|
||||||
|
|
||||||
|
toolchain go1.22.2
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883
|
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883
|
||||||
github.com/digineo/go-ping v1.0.0
|
github.com/digineo/go-ping v1.0.1
|
||||||
github.com/gokrazy/gokrazy v0.0.0-20200501080617-f3445e01a904
|
github.com/eclipse/paho.mqtt.golang v1.4.1
|
||||||
github.com/gokrazy/internal v0.0.0-20200407080221-9da902858268 // indirect
|
github.com/gokrazy/gokrazy v0.0.0-20230812092215-346db1998f83
|
||||||
github.com/golang/protobuf v1.4.1 // indirect
|
github.com/google/go-cmp v0.6.0
|
||||||
github.com/google/go-cmp v0.4.0
|
github.com/google/gopacket v1.1.19
|
||||||
github.com/google/gopacket v1.1.17
|
github.com/google/nftables v0.2.1-0.20240422065334-aa8348f7904c
|
||||||
github.com/google/nftables v0.0.0-20200316075819-7127d9d22474
|
github.com/google/renameio v1.0.1
|
||||||
github.com/google/renameio v0.1.0
|
github.com/insomniacslk/dhcp v0.0.0-20220822114210-de18a9d48e84
|
||||||
github.com/insomniacslk/dhcp v0.0.0-20200420235442-ed3125c2efe7
|
|
||||||
github.com/jpillora/backoff v1.0.0
|
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/krolaw/dhcp4 v0.0.0-20190909130307-a50d88189771
|
||||||
github.com/libdns/cloudflare v0.0.0-20200528144945-97886e7873b1
|
github.com/libdns/cloudflare v0.1.0
|
||||||
github.com/libdns/libdns v0.0.0-20200501023120-186724ffc821
|
github.com/libdns/libdns v0.2.1
|
||||||
github.com/mdlayher/ndp v0.0.0-20200509194142-8a50b5ef8b52
|
github.com/mdlayher/ethtool v0.1.0
|
||||||
github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065
|
github.com/mdlayher/ndp v0.10.0
|
||||||
github.com/miekg/dns v1.1.29
|
github.com/mdlayher/packet v1.1.2
|
||||||
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
|
github.com/miekg/dns v1.1.50
|
||||||
github.com/prometheus/client_golang v1.6.0
|
github.com/prometheus/client_golang v1.19.0
|
||||||
github.com/rtr7/dhcp4 v0.0.0-20181120124042-778e8c2e24a5
|
github.com/rtr7/dhcp4 v0.0.0-20220302171438-18c84d089b46
|
||||||
github.com/sergi/go-diff v1.1.0 // indirect
|
github.com/vishvananda/netlink v1.2.1-beta.2
|
||||||
github.com/u-root/u-root v6.0.0+incompatible // indirect
|
github.com/vishvananda/netns v0.0.4
|
||||||
github.com/vishvananda/netlink v1.1.0
|
golang.org/x/crypto v0.31.0
|
||||||
golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79
|
golang.org/x/net v0.23.0
|
||||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f
|
golang.org/x/sync v0.7.0
|
||||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a
|
golang.org/x/sys v0.28.0
|
||||||
golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f
|
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9
|
||||||
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20220504211119-3d4a969bb56b
|
||||||
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
|
require (
|
||||||
gopkg.in/yaml.v2 v2.2.8 // indirect
|
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
|
||||||
)
|
)
|
||||||
|
356
go.sum
356
go.sum
@ -1,265 +1,221 @@
|
|||||||
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 h1:bvNMNQO63//z+xNgfBlViaCIJKLlCJ6/fmUseuG0wVQ=
|
||||||
github.com/andreyvit/diff v0.0.0-20170406064948-c7f18ee00883/go.mod h1:rCTlJbsFo29Kk6CurOXKm700vrz8f0KW0JNfpkRJY/8=
|
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 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/cespare/xxhash/v2 v2.1.1 h1:6MnRN8NT7+YBpUIWxHtefFZOKTAPgGjpQSxqLNn0+qY=
|
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||||
github.com/cespare/xxhash/v2 v2.1.1/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.2.0/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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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 h1:OT/LKmj81wMymnWXaKaKBR9n1vPlu+GC0VVKaZP6kzs=
|
||||||
github.com/digineo/go-logwrap v0.0.0-20181106161722-a178c58ea3f0/go.mod h1:DmqdumeAKGQNU5E8MN0ruT5ZGx8l/WbAsMbXCXcSEts=
|
github.com/digineo/go-logwrap v0.0.0-20181106161722-a178c58ea3f0/go.mod h1:DmqdumeAKGQNU5E8MN0ruT5ZGx8l/WbAsMbXCXcSEts=
|
||||||
github.com/digineo/go-ping v1.0.0 h1:gOuD3YzkIcW/0Y2IAe27bsMKtpfNZdoX1Rnc1RGYOSI=
|
github.com/digineo/go-ping v1.0.1 h1:Yn9hwM0RY4j4D3gcmLvRJf0d7MrbucfUhnOeVDvcVyk=
|
||||||
github.com/digineo/go-ping v1.0.0/go.mod h1:YLDBnHoAygacawa2aubI4vXhZ4do5f62oJSvRiJVEjw=
|
github.com/digineo/go-ping v1.0.1/go.mod h1:uCbFC0VUqGNBNiev44BGSxfOrEAmC73GjpRje1l40Zo=
|
||||||
github.com/fatih/color v1.7.0 h1:DkWD4oS2D8LGGgTQ6IvwJJXSL5Vp2ffcQg58nFV38Ys=
|
github.com/eclipse/paho.mqtt.golang v1.4.1 h1:tUSpviiL5G3P9SZZJPC4ZULZJsxQKXxfENpMvdbAXAI=
|
||||||
github.com/fatih/color v1.7.0/go.mod h1:Zm6kSWBoL9eyXnKyktHP6abPY2pDugNf5KwzbycvMj4=
|
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/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
github.com/gdamore/encoding v1.0.0/go.mod h1:alR0ol34c49FCSBLjhosxzcPHQbf2trDkoo5dl+VrEg=
|
||||||
github.com/gdamore/tcell v1.1.1/go.mod h1:K1udHkiR3cOtlpKG5tZPD5XxrF7v2y7lDq7Whcj+xkQ=
|
github.com/gdamore/tcell/v2 v2.0.1-0.20201017141208-acf90d56d591/go.mod h1:vSVL/GV5mCSlPC6thFP5kfOFdM9MGZcalipmpTxTgQA=
|
||||||
github.com/go-kit/kit v0.8.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
github.com/gokrazy/gokrazy v0.0.0-20230812092215-346db1998f83 h1:Y4sADvUYd/c0eqnqebipHHl0GMpAxOQeTzPnwI4ievM=
|
||||||
github.com/go-kit/kit v0.9.0/go.mod h1:xBxKIO96dXMWWy0MnWVtmwkA9/13aqxPnvrjFYMA2as=
|
github.com/gokrazy/gokrazy v0.0.0-20230812092215-346db1998f83/go.mod h1:9q5Tg+q+YvRjC3VG0gfMFut46dhbhtAnvUEp4lPjc6c=
|
||||||
github.com/go-logfmt/logfmt v0.3.0/go.mod h1:Qt1PoO58o5twSAckw1HlFXLmHsOX5/0LbT9GBnD5lWE=
|
github.com/gokrazy/internal v0.0.0-20230211171410-9608422911d0 h1:QTi0skQ/OM7he/5jEWA9k/DYgdwGAhw3hrUoiPGGZHM=
|
||||||
github.com/go-logfmt/logfmt v0.4.0/go.mod h1:3RMwSq7FuexP4Kalkev3ejPJsZTpXXBr9+V4qmtdjCk=
|
github.com/gokrazy/internal v0.0.0-20230211171410-9608422911d0/go.mod h1:ddHcxXZ/VVQOSAWcRBbkYY58+QOw4L145ye6phyDmRA=
|
||||||
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.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.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.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.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/go-cmp v0.4.1 h1:/exdXoGamhu5ONeUJH0deniYLWYvQwW66yvlfiiKTu0=
|
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
github.com/google/gopacket v1.1.16/go.mod h1:UCLx9mCmAwsVbn6qQl1WIEt2SO7Nd2fD0th1TBAsqBw=
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
github.com/google/gopacket v1.1.17 h1:rMrlX2ZY2UbvT+sdz3+6J+pp2z+msCq9MxTU6ymxbBY=
|
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
|
||||||
github.com/google/gopacket v1.1.17/go.mod h1:UdDNZ1OO62aGYVnPhxT1U6aI7ukYtA/kB8vaU0diBUM=
|
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
|
||||||
github.com/google/nftables v0.0.0-20200316075819-7127d9d22474 h1:D6bN82zzK92ywYsE+Zjca7EHZCRZbcNTU3At7WdxQ+c=
|
github.com/google/nftables v0.2.1-0.20240422065334-aa8348f7904c h1:XJHEjE/d9/F9Sp6hvRCfh6Sl4WtCoKx7JJI2z1trH/Y=
|
||||||
github.com/google/nftables v0.0.0-20200316075819-7127d9d22474/go.mod h1:cfspEyr/Ap+JDIITA+N9a0ernqG0qZ4W1aqMRgDZa1g=
|
github.com/google/nftables v0.2.1-0.20240422065334-aa8348f7904c/go.mod h1:Fo/xFnOxWlRQtnHdNi46KbIjufTDzbKhtghpWrmsSUg=
|
||||||
github.com/google/renameio v0.1.0 h1:GOZbcHa3HfsPKPlmyPyN2KEohoMXOhdMbHrvbpl2QaA=
|
github.com/google/renameio v1.0.1 h1:Lh/jXZmvZxb0BBeSY5VKEfidcbcbenKjZFzM/q0fSeU=
|
||||||
github.com/google/renameio v0.1.0/go.mod h1:KWCgfxg9yswjAJkECMjeO8J8rahYeXnNhOm40UhjYkI=
|
github.com/google/renameio v1.0.1/go.mod h1:t/HQoYBZSsWSNK35C6CO/TpPLDVWvxOHboWUAweKUpk=
|
||||||
github.com/insomniacslk/dhcp v0.0.0-20200420235442-ed3125c2efe7 h1:iaCm+9nZdYb8XCSU2TfIb0qYTcAlIv2XzyKR2d2xZ38=
|
github.com/google/renameio/v2 v2.0.0 h1:UifI23ZTGY8Tt29JbYFiuyIU3eX+RNFtUwefq9qAhxg=
|
||||||
github.com/insomniacslk/dhcp v0.0.0-20200420235442-ed3125c2efe7/go.mod h1:CfMdguCK66I5DAUJgGKyNz8aB6vO5dZzkm9Xep6WGvw=
|
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/jpillora/backoff v1.0.0 h1:uvFg412JmmHBHw7iwprIxkPMI+sGQ4kzOWsMeHnm2EA=
|
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/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-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-20200117123717-f846d4f6c1f4/go.mod h1:WGuG/smIU4J/54PblvSbh+xvCZmpJnFgr3ds6Z55XMQ=
|
||||||
github.com/json-iterator/go v1.1.6/go.mod h1:+SdeFBvtyEkXs7REEP0seUULqWtbJapLOCVDaaPEHmU=
|
github.com/jsimonetti/rtnetlink v0.0.0-20201009170750-9c6f07d100c1/go.mod h1:hqoO/u39cqLeBLebZ8fWdE96O7FxrAsRYhnVOdgHxok=
|
||||||
github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/uOdHXbAo4=
|
github.com/jsimonetti/rtnetlink v0.0.0-20201110080708-d2c240429e6c/go.mod h1:huN4d1phzjhlOsNIjFsw2SVRbwIHj3fJDMEU2SDPTmg=
|
||||||
github.com/julienschmidt/httprouter v1.2.0/go.mod h1:SYymIcj16QtmaHHD7aYtjjsJG7VTCxuUUipMqKk8s4w=
|
github.com/jtolds/gls v4.20.0+incompatible/go.mod h1:QJZ7F/aHp+rZTRtaJ1ow/lLfFfVYBRgL+9YlvaHOwJU=
|
||||||
github.com/koneu/natend v0.0.0-20150829182554-ec0926ea948d h1:MFX8DxRnKMY/2M3H61iSsVbo/n3h0MWGmWNN1UViOU0=
|
github.com/kenshaw/evdev v0.1.0 h1:wmtceEOFfilChgdNT+c/djPJ2JineVsQ0N14kGzFRUo=
|
||||||
github.com/koneu/natend v0.0.0-20150829182554-ec0926ea948d/go.mod h1:QHb4k4cr1fQikUahfcRVPcEXiUgFsdIstGqlurL0XL4=
|
github.com/kenshaw/evdev v0.1.0/go.mod h1:B/fErKCihUyEobz0mjn2qQbHgyJKFQAxkXSvkeeA/Wo=
|
||||||
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/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/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.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 h1:t2c2B9g1ZVhMYduqmANSEGVD3/1WlsrEYNPtVoFlENk=
|
||||||
github.com/krolaw/dhcp4 v0.0.0-20190909130307-a50d88189771/go.mod h1:0AqAH3ZogsCrvrtUpvc6EtVKbc3w6xwZhkvGLuqyi3o=
|
github.com/krolaw/dhcp4 v0.0.0-20190909130307-a50d88189771/go.mod h1:0AqAH3ZogsCrvrtUpvc6EtVKbc3w6xwZhkvGLuqyi3o=
|
||||||
github.com/libdns/cloudflare v0.0.0-20200528144945-97886e7873b1 h1:Jx0AoxHtj2NMwxHByM8VmcqvGMa3lEu28xVDArhSi7E=
|
github.com/libdns/cloudflare v0.1.0 h1:93WkJaGaiXCe353LHEP36kAWCUw0YjFqwhkBkU2/iic=
|
||||||
github.com/libdns/cloudflare v0.0.0-20200528144945-97886e7873b1/go.mod h1:A9MqNmkZcd81mY7JsNysmgmj5O9vlRjfDVaNw4j9pjU=
|
github.com/libdns/cloudflare v0.1.0/go.mod h1:a44IP6J1YH6nvcNl1PverfJviADgXUnsozR3a7vBKN8=
|
||||||
github.com/libdns/libdns v0.0.0-20200430163404-ee2c42449104/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
|
github.com/libdns/libdns v0.2.0/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
|
||||||
github.com/libdns/libdns v0.0.0-20200501023120-186724ffc821 h1:663opx/RKxiISi1ozf0WbvweQpYBgf34dx8hKSIau3w=
|
github.com/libdns/libdns v0.2.1 h1:Wu59T7wSHRgtA0cfxC+n1c/e+O3upJGWytknkmFEDis=
|
||||||
github.com/libdns/libdns v0.0.0-20200501023120-186724ffc821/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
|
github.com/libdns/libdns v0.2.1/go.mod h1:yQCXzk1lEZmmCPa857bnk4TsOiqYasqpyOEeSObbb40=
|
||||||
github.com/lucasb-eyer/go-colorful v0.0.0-20181028223441-12d3b2882a08/go.mod h1:NXg0ArsFk0Y01623LgUqoqcouGDB+PwCCQlrwrG6xJ4=
|
github.com/lucasb-eyer/go-colorful v1.0.3/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
||||||
github.com/mattn/go-colorable v0.0.9/go.mod h1:9vuHe8Xs5qXnSaW/c/ABM9alt+Vo+STaOChaDxuIBZU=
|
github.com/mattn/go-colorable v0.1.8/go.mod h1:u6P/XSegPjTcexA+o6vUJrdnUu04hMope9wVRipJSqc=
|
||||||
github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4=
|
github.com/mattn/go-isatty v0.0.12/go.mod h1:cbi8OIDigv2wuxKPP5vlRcQ1OAZbq2CE4Kysco4FUpU=
|
||||||
github.com/mattn/go-runewidth v0.0.4/go.mod h1:LwmH8dsx7+W8Uxz3IHJYH5QSwggIsqBzpuz5H//U1FU=
|
github.com/mattn/go-runewidth v0.0.7/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
github.com/mattn/go-runewidth v0.0.9/go.mod h1:H031xJmbD/WCDINGzjvQ9THkh0rPKHF+m2gUSrubnMI=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
github.com/mdlayher/ethernet v0.0.0-20190606142754-0394541c37b7/go.mod h1:U6ZQobyTjI/tJyq2HG+i/dfSoFUt8/aZCM+GKtmFk/Y=
|
||||||
github.com/mdlayher/genetlink v1.0.0 h1:OoHN1OdyEIkScEmRgxLEe2M9U8ClMytqA5niynLtfj0=
|
github.com/mdlayher/ethtool v0.1.0 h1:XAWHsmKhyPOo42qq/yTPb0eFBGUKKTR1rE0dVrWVQ0Y=
|
||||||
github.com/mdlayher/genetlink v1.0.0/go.mod h1:0rJ0h4itni50A86M2kHcgS85ttZazNt7a8H2a2cw0Gc=
|
github.com/mdlayher/ethtool v0.1.0/go.mod h1:fBMLn2UhfRGtcH5ZFjr+6GUiHEjZsItFD7fSn7jbZVQ=
|
||||||
github.com/mdlayher/ndp v0.0.0-20200509194142-8a50b5ef8b52 h1:qWqNvHaKhGECNieU1gGusKRuoPeoR+rhlkaWdO1gyT8=
|
github.com/mdlayher/genetlink v1.3.2 h1:KdrNKe+CTu+IbZnm/GVUMXSqBBLqcGpRDa0xkQy56gw=
|
||||||
github.com/mdlayher/ndp v0.0.0-20200509194142-8a50b5ef8b52/go.mod h1:AXE3T2f7eg/MV02LS+DGHgH0c+ehknWViE4pgbHtZf8=
|
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/mdlayher/netlink v0.0.0-20190409211403-11939a169225/go.mod h1:eQB3mZE4aiYnlUsyGGCOpPETfdQq4Jhsgf1fk3cwQaA=
|
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.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.0/go.mod h1:H4WCitaheIsdF9yOYu8CFmCgQthAPIWZmcKp9uZHgmY=
|
||||||
github.com/mdlayher/raw v0.0.0-20190303161257-764d452d77af/go.mod h1:rC/yE65s/DoHB6BzVOUBNYBGTg772JVytyAytffIZkY=
|
github.com/mdlayher/netlink v1.1.1/go.mod h1:WTYpFb/WTvlRJAyKhZL5/uy69TDDpHHu2VZmb2XgV7o=
|
||||||
github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065 h1:aFkJ6lx4FPip+S+Uw4aTegFMct9shDvP+79PsSxpm3w=
|
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-20191009151244-50f2db8cc065/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg=
|
github.com/mdlayher/raw v0.0.0-20191009151244-50f2db8cc065/go.mod h1:7EpbotpCmVZcu+KCX4g9WaRNuu11uyhiW7+Le1dKawg=
|
||||||
github.com/miekg/dns v1.1.29 h1:xHBEhR+t5RzcFJjBLJlax2daXOrTYtr9z4WdKEfWFzg=
|
github.com/mdlayher/socket v0.5.0 h1:ilICZmJcQz70vrWVes1MFera4jGiWNocSkykwwoy3XI=
|
||||||
github.com/miekg/dns v1.1.29/go.mod h1:KNUDUusw/aVsxyTYZM1oqvCicbwhgbNgztCETuNZ7xM=
|
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/mikioh/ipaddr v0.0.0-20190404000644-d465c8ab6721 h1:RlZweED6sbSArvlE924+mUcZuXKLBHA35U7LN621Bws=
|
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/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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
github.com/prometheus/client_golang v0.9.1/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXPKyh/dDVn+NZz0KFw=
|
github.com/prometheus/client_golang v1.19.0 h1:ygXvpU1AoN1MhdzckN+PyD9QJOSD4x7kmXYlnfbA6JU=
|
||||||
github.com/prometheus/client_golang v1.0.0/go.mod h1:db9x61etRT2tGnBNRi70OPL5FsnadC4Ky3P0J6CfImo=
|
github.com/prometheus/client_golang v1.19.0/go.mod h1:ZRM9uEAypZakd+q/x7+gmsvXdURP+DABIEIjnmDdp+k=
|
||||||
github.com/prometheus/client_golang v1.6.0 h1:YVPodQOcK15POxhgARIvnDRVpLcuK8mglnMrWfyrw6A=
|
github.com/prometheus/client_model v0.6.1 h1:ZKSh/rekM+n3CeS952MLRAdFwIKqeY8b62p8ais2e9E=
|
||||||
github.com/prometheus/client_golang v1.6.0/go.mod h1:ZLOG9ck3JLRdB5MgO8f+lLTe83AXG6ro35rLTxvnIl4=
|
github.com/prometheus/client_model v0.6.1/go.mod h1:OrxVMOVHjw3lKMa8+x6HeMGkHMQyHDk9E3jmP2AmGiY=
|
||||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
github.com/prometheus/common v0.53.0 h1:U2pL9w9nmJwJDa4qqLQ3ZaePJ6ZTwt7cMD3AG3+aLCE=
|
||||||
github.com/prometheus/client_model v0.0.0-20190129233127-fd36f4220a90/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
github.com/prometheus/common v0.53.0/go.mod h1:BrxBKv3FWBIGXw89Mg1AeBq7FSyRzXWI3l3e7W3RN5U=
|
||||||
github.com/prometheus/client_model v0.2.0 h1:uq5h0d+GuxiXLJLNABMgp2qUWDPiLvgCzz2dUR+/W/M=
|
github.com/prometheus/procfs v0.14.0 h1:Lw4VdGGoKEZilJsayHf0B+9YgLGREba2C6xr+Fdfq6s=
|
||||||
github.com/prometheus/client_model v0.2.0/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
github.com/prometheus/procfs v0.14.0/go.mod h1:XL+Iwz8k8ZabyZfMFHPiilCniixqQarAy5Mu67pHlNQ=
|
||||||
github.com/prometheus/common v0.4.1/go.mod h1:TNfzLD0ON7rHzMJeJkieUDPYmFC7Snx/y86RQel1bk4=
|
github.com/rivo/tview v0.0.0-20201204190810-5406288b8e4e/go.mod h1:0ha5CGekam8ZV1kxkBxSlh7gfQ7YolUj2P/VruwH0QY=
|
||||||
github.com/prometheus/common v0.9.1 h1:KOMtN28tlbam3/7ZKEYKHhKoJZYYj3gMH4uc62x7X7U=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/prometheus/common v0.9.1/go.mod h1:yhUN8i9wzaXS3w1O07YhxHEBxD+W35wd8bs7vj7HSQ4=
|
github.com/rtr7/dhcp4 v0.0.0-20220302171438-18c84d089b46 h1:3psQveH4RUiv5yc3p7kRySilf1nSXLQhAvJFwg4fgnE=
|
||||||
github.com/prometheus/procfs v0.0.0-20181005140218-185b4288413d/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
github.com/rtr7/dhcp4 v0.0.0-20220302171438-18c84d089b46/go.mod h1:Ng1F/s+z0zCMsbEFEneh+30LJa9DrTfmA+REbEqcTPk=
|
||||||
github.com/prometheus/procfs v0.0.2/go.mod h1:TjEm7ze935MbeOT/UhFTIMYKhuLP4wbCsTZCD3I8kEA=
|
github.com/sergi/go-diff v1.2.0 h1:XU+rvMAioB0UC3q1MFrIQy4Vo5/4VsRDQQXHsEya6xQ=
|
||||||
github.com/prometheus/procfs v0.0.11 h1:DhHlBtkHWPYi8O2y31JkK0TF+DGM+51OopZjH/Ia5qI=
|
github.com/sergi/go-diff v1.2.0/go.mod h1:STckp+ISIX8hZLjrqAeVduY0gWCT9IjLuqbuNXdaHfM=
|
||||||
github.com/prometheus/procfs v0.0.11/go.mod h1:lV6e/gmhEcM9IjHGsFOCxxuZ+z1YqCvr4OA4YeYWdaU=
|
github.com/smartystreets/assertions v0.0.0-20180927180507-b2de0cb4f26d/go.mod h1:OnSkiWE9lh6wB0YB77sQom3nweQdgAjqCqsofrRNTgc=
|
||||||
github.com/rivo/tview v0.0.0-20181226202439-36893a669792/go.mod h1:J4W+hErFfITUbyFAEXizpmkuxX7ZN56dopxHB4XQhMw=
|
github.com/smartystreets/goconvey v1.6.4/go.mod h1:syvi0/a8iFYH4r/RixwvyeAJjdLS9QV7WQ/tjFTllLA=
|
||||||
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.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||||
github.com/stretchr/objx v0.1.1 h1:2vfRuCMp5sSVIDSqO8oNnWJq7mPa6KVP3iPIwFBuy8A=
|
github.com/stretchr/objx v0.2.0 h1:Hbg2NidpLE8veEBkEZTL3CvlkUIVzuU9jDplZO54c48=
|
||||||
github.com/stretchr/objx v0.1.1/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
github.com/stretchr/objx v0.2.0/go.mod h1:qt09Ya8vawLte6SNmTgCsAVtYtaKzEcn8ATUoHMkEqE=
|
||||||
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.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.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||||
github.com/stretchr/testify v1.5.1 h1:nOGnQDM7FYENwehXlg/kFVnos3rEvtKTjRvOWSzb6H4=
|
github.com/stretchr/testify v1.6.1 h1:hDPOHmpOpP40lSULcqw7IrRb/u7w6RpDC9399XyoNd0=
|
||||||
github.com/stretchr/testify v1.5.1/go.mod h1:5W2xD1RspED5o8YsWQXVCued0rvSQ+mT+I5cxcmMvtA=
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
github.com/u-root/u-root v6.0.0+incompatible h1:YqPGmRoRyYmeg17KIWFRSyVq6LX5T6GSzawyA6wG6EE=
|
github.com/u-root/uio v0.0.0-20210528114334-82958018845c h1:BFvcl34IGnw8yvJi8hlqLFo9EshRInwWBs2M5fGWzQA=
|
||||||
github.com/u-root/u-root v6.0.0+incompatible/go.mod h1:RYkpo8pTHrNjW08opNd/U6p/RJE7K0D8fXO0d47+3YY=
|
github.com/u-root/uio v0.0.0-20210528114334-82958018845c/go.mod h1:LpEX5FO/cB+WF4TYGY1V5qktpaZLkKkSegbr0V4eYXA=
|
||||||
github.com/vishvananda/netlink v1.1.0 h1:1iyaYNBLmP6L0220aDnYQpo1QEV4t4hJ+xEEhhJH8j0=
|
github.com/vishvananda/netlink v1.2.1-beta.2 h1:Llsql0lnQEbHj0I1OuKyp8otXp0r3q0mPkuhwHfStVs=
|
||||||
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
|
github.com/vishvananda/netlink v1.2.1-beta.2/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
|
||||||
github.com/vishvananda/netns v0.0.0-20180720170159-13995c7128cc/go.mod h1:ZjcWmFBXmLKZu9Nxj3WKYEafiSqer2rnvPr0en9UNpI=
|
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
|
||||||
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df h1:OviZH7qLw/7ZovXvuNyL3XQl8UFofeikI1NW1Gypu7k=
|
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
|
||||||
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
|
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||||
|
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
|
||||||
gitlab.com/golang-commonmark/puny v0.0.0-20191124015043-9f83538fa04f h1:Wku8eEdeJqIOFHtrfkYUByc4bCaTeA6fL0UJgfEiFMI=
|
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=
|
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-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-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||||
golang.org/x/crypto v0.0.0-20191206172530-e9b2fee46413/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||||
golang.org/x/crypto v0.0.0-20200204104054-c9f3fb736b72/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/crypto v0.31.0 h1:ihbySMvVjLAeSH1IbfcRTkD/iNscyz8rGzjF/E5hV6U=
|
||||||
golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79 h1:IaQbIIB2X/Mp/DKctl6ROxz1KyMlKp4uyvL6+kQ7C88=
|
golang.org/x/crypto v0.31.0/go.mod h1:kDsLvtWBEx7MV9tJOj9bnXsPbxwJQ6csT/x4KIN4Ssk=
|
||||||
golang.org/x/crypto v0.0.0-20200429183012-4b2356b1ed79/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||||
golang.org/x/net v0.0.0-20181114220301-adae6a3d119a/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
|
||||||
golang.org/x/net v0.0.0-20181220203305-927f97764cc3/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
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-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
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-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-20190419010253-1f3472d942ba/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||||
golang.org/x/net v0.0.0-20190613194153-d28f0bde5980/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
golang.org/x/net v0.0.0-20190603091049-60506f45cf65/go.mod h1:HSz+uSET+XFnRR8LxR5pz3Of3rY3CfYBVs4xY44aLks=
|
||||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/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-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-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-20200202094626-16171245cfb2/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f h1:QBjCr1Fz5kw158VqdE9JfI9cJnl/ymnJWAdMuinqL7Y=
|
golang.org/x/net v0.0.0-20200425230154-ff2c4b7c35a0/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
||||||
golang.org/x/net v0.0.0-20200506145744-7e3656a0809f/go.mod h1:qpuaurCH72eLCgpAm/N6yyVIVM9cpaDIP3A8BGJEC5A=
|
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/net v0.0.0-20201110031124-69a78807bb2b/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||||
golang.org/x/sync v0.0.0-20181221193216-37e7f081c4d4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
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/sync v0.0.0-20190423024810-112230192c58/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-20190911185100-cd5d95a43a6e h1:vcxGaoTs7kV8m5Np9uUNQin4BrLOthgV7252N8V+FwY=
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
golang.org/x/sync v0.7.0 h1:YsImfSBoP9QPYL0xyKJPq0gcaJdG3rInoqxTWbfQu9M=
|
||||||
golang.org/x/sync v0.0.0-20200317015054-43a5402ce75a h1:WXEvlFVvvGxCJLG6REjsT03iWnKLEWinaScsxF2Vm2o=
|
golang.org/x/sync v0.7.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
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-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-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-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-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-20190418153312-f0ce4c0180be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190422165155-953cdadca894/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190606122018-79a91cf218c4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20190626150813-e07cf5db2756/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20190826190057-c7b8b68b1456/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-20191008105621-543471e840be/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20191029155521-f43be2a4598c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200116001909-b77594299b42/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-20200202164722-d101bd2416d5/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200302150141-5c8b2ff67527/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd h1:xhmwyvizuTgC2qz7ZlMluP20uW+C3Rm0FD/WLDX8884=
|
golang.org/x/sys v0.0.0-20200223170610-d5e6a3e2c0ae/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200406155108-e3b113bbe6a4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f h1:gWF768j/LaZugp8dyS4UwsslYCYz9XgFxvlgsn0n9H8=
|
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200420163511-1957bb5e6d1f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201009025420-dfb3f7c4e634/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f h1:mOhmO9WsBaJCNmaZHPtHs9wOcdqdKCjF6OPJlmDM3KI=
|
golang.org/x/sys v0.0.0-20201017003518-b09fb700fbb7/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/sys v0.0.0-20200509044756-6aff5f38e54f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
golang.org/x/sys v0.0.0-20201101102859-da207088b7d1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
golang.org/x/text v0.3.0 h1:g61tztE5qeGQ89tm6NTjjM9VPIm088od1l6aSorWRWg=
|
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/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||||
golang.org/x/text v0.3.2 h1:tW2bmiBqwgJj/UpqtC8EpXEZVYOwU0yG4iWbprSVAcs=
|
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
|
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||||
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1 h1:NusfzzA6yGQ+ua51ck7E3omNUX/JuqbFSaRGqU8CcLI=
|
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9 h1:ftMN5LMiBFjbzleLqtoBZk7KdJwhuybIU+FckUHgoyQ=
|
||||||
golang.org/x/time v0.0.0-20200416051211-89c76fbcd5d1/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
golang.org/x/time v0.0.0-20220722155302-e5dcc9cfc0b9/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-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
golang.org/x/tools v0.0.0-20191216052735-49a3e744a425/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
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/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
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-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.zx2c4.com/wireguard v0.0.20200121 h1:vcswa5Q6f+sylDfjqyrVNNrjsFUUbPsgAQTBCAg/Qf8=
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1 h1:go1bK/D/BFZV2I8cIQd1NKEZ+0owSTG1fDTci4IqFcE=
|
||||||
golang.zx2c4.com/wireguard v0.0.20200121/go.mod h1:P2HsVp8SKwZEufsnezXZA4GRX/T49/HlU7DGuelXsU4=
|
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
golang.zx2c4.com/wireguard v0.0.20200320 h1:1vE6zVeO7fix9cJX1Z9ZQ+ikPIIx7vIyU0o0tLDD88g=
|
golang.zx2c4.com/wireguard v0.0.0-20220407013110-ef5c587f782d h1:q4JksJ2n0fmbXC0Aj0eOs6E0AcPqnKglxWXWFqGD6x0=
|
||||||
golang.zx2c4.com/wireguard v0.0.20200320/go.mod h1:lDian4Sw4poJ04SgHh35nzMVwGSYlPumkdnHcucAQoY=
|
golang.zx2c4.com/wireguard v0.0.0-20220407013110-ef5c587f782d/go.mod h1:bVQfyl2sCM/QIIGHpWbFGfHPuDvqnCNkT6MQLTCjO/U=
|
||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20200324154536-ceff61240acf h1:rWUZHukj3poXegPQMZOXgxjTGIBe3mLNHNVvL5DsHus=
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20220504211119-3d4a969bb56b h1:9JncmKXcUwE918my+H6xmjBdhK2jM/UTUNXxhRG1BAk=
|
||||||
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20200324154536-ceff61240acf/go.mod h1:UdS9frhv65KTfwxME1xE8+rHYoFpbm36gOud1GhBe9c=
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20220504211119-3d4a969bb56b/go.mod h1:yp4gl6zOlnDGOZeWeDfMwQcsdOIQnMdhuPx9mwwWBL4=
|
||||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||||
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 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-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/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.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v2 v2.2.4/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||||
gopkg.in/yaml.v2 v2.2.5 h1:ymVxjfMaHvXD8RqPRmzHHsB3VvucivSkIAvJFDI5O3c=
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v2 v2.2.5/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||||
gopkg.in/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
gopkg.in/yaml.v2 v2.2.8/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
|
||||||
|
51
init/init.go
51
init/init.go
@ -1,51 +0,0 @@
|
|||||||
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,6 +28,7 @@ import (
|
|||||||
|
|
||||||
"github.com/rtr7/router7/internal/netconfig"
|
"github.com/rtr7/router7/internal/netconfig"
|
||||||
"github.com/vishvananda/netlink"
|
"github.com/vishvananda/netlink"
|
||||||
|
"github.com/vishvananda/netns"
|
||||||
|
|
||||||
"github.com/andreyvit/diff"
|
"github.com/andreyvit/diff"
|
||||||
"github.com/google/go-cmp/cmp"
|
"github.com/google/go-cmp/cmp"
|
||||||
@ -45,11 +46,21 @@ const goldenInterfaces = `
|
|||||||
"hardware_addr": "02:73:53:00:b0:0c",
|
"hardware_addr": "02:73:53:00:b0:0c",
|
||||||
"spoof_hardware_addr": "02:73:53:00:b0:aa",
|
"spoof_hardware_addr": "02:73:53:00:b0:aa",
|
||||||
"name": "lan0",
|
"name": "lan0",
|
||||||
"addr": "192.168.42.1/24"
|
"addr": "192.168.42.1/24",
|
||||||
|
"mtu": 1492
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "wg0",
|
"name": "wg0",
|
||||||
"addr": "fe80::1/64"
|
"addr": "fe80::1/64",
|
||||||
|
"extra_addrs": [
|
||||||
|
"10.22.100.1/24"
|
||||||
|
],
|
||||||
|
"extra_routes": [
|
||||||
|
{
|
||||||
|
"destination": "2a02:168:4a00:22::/64",
|
||||||
|
"gateway": "fe80::2"
|
||||||
|
}
|
||||||
|
]
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
@ -137,19 +148,24 @@ func goldenNftablesRules(additionalForwarding bool) string {
|
|||||||
add := ""
|
add := ""
|
||||||
if additionalForwarding {
|
if additionalForwarding {
|
||||||
add = `
|
add = `
|
||||||
iifname "uplink0" tcp dport 8045 dnat to 192.168.42.22:8045`
|
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`
|
||||||
}
|
}
|
||||||
return `table ip nat {
|
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 {
|
chain prerouting {
|
||||||
type nat hook prerouting priority 0; policy accept;
|
type nat hook prerouting priority 0; policy accept;
|
||||||
iifname "uplink0" tcp dport 8080 dnat to 192.168.42.23:9999` + add + `
|
jump router7-portforwardings
|
||||||
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 {
|
chain postrouting {
|
||||||
type nat hook postrouting priority 100; policy accept;
|
type nat hook postrouting priority 100; policy accept;
|
||||||
oifname "uplink0" masquerade
|
oifname "uplink0" masquerade
|
||||||
|
iifname "lan0" oifname "lan0" ct status 0x20 masquerade
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
table ip filter {
|
table ip filter {
|
||||||
@ -157,14 +173,11 @@ table ip filter {
|
|||||||
packets 23 bytes 42
|
packets 23 bytes 42
|
||||||
}
|
}
|
||||||
|
|
||||||
chain forward {
|
counter inputc {
|
||||||
type filter hook forward priority 0; policy accept;
|
packets 23 bytes 42
|
||||||
oifname "uplink0" tcp flags 0x2 tcp option maxseg size set rt mtu
|
|
||||||
counter name "fwded"
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
table ip6 filter {
|
counter outputc {
|
||||||
counter fwded {
|
|
||||||
packets 23 bytes 42
|
packets 23 bytes 42
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -173,6 +186,45 @@ table ip6 filter {
|
|||||||
oifname "uplink0" tcp flags 0x2 tcp option maxseg size set rt mtu
|
oifname "uplink0" tcp flags 0x2 tcp option maxseg size set rt mtu
|
||||||
counter name "fwded"
|
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"
|
||||||
|
}
|
||||||
}`
|
}`
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -202,17 +254,27 @@ const goldenDhcp6 = `
|
|||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
type wgLink struct{}
|
type wgLink struct {
|
||||||
|
ns int
|
||||||
|
}
|
||||||
|
|
||||||
func (w *wgLink) Type() string { return "wireguard" }
|
func (w *wgLink) Type() string { return "wireguard" }
|
||||||
|
|
||||||
func (w *wgLink) Attrs() *netlink.LinkAttrs {
|
func (w *wgLink) Attrs() *netlink.LinkAttrs {
|
||||||
attrs := netlink.NewLinkAttrs()
|
attrs := netlink.NewLinkAttrs()
|
||||||
attrs.Name = "wg5"
|
attrs.Name = "wg5"
|
||||||
|
if w.ns > 0 {
|
||||||
|
attrs.Namespace = netlink.NsFd(w.ns)
|
||||||
|
}
|
||||||
return &attrs
|
return &attrs
|
||||||
}
|
}
|
||||||
|
|
||||||
var wireGuardAvailable = func() bool {
|
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
|
// ns must not collide with any namespace used in the test functions: this
|
||||||
// function will be called by the helper process, too.
|
// function will be called by the helper process, too.
|
||||||
const ns = "ns4"
|
const ns = "ns4"
|
||||||
@ -223,7 +285,18 @@ var wireGuardAvailable = func() bool {
|
|||||||
}
|
}
|
||||||
defer exec.Command("ip", "netns", "delete", ns).Run()
|
defer exec.Command("ip", "netns", "delete", ns).Run()
|
||||||
|
|
||||||
return netlink.LinkAdd(&wgLink{}) == nil
|
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
|
||||||
}()
|
}()
|
||||||
|
|
||||||
func TestNetconfig(t *testing.T) {
|
func TestNetconfig(t *testing.T) {
|
||||||
@ -265,7 +338,7 @@ func TestNetconfig(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
netconfig.DefaultCounterObj = &nftables.CounterObj{Packets: 23, Bytes: 42}
|
netconfig.DefaultCounterObj = &nftables.CounterObj{Packets: 23, Bytes: 42}
|
||||||
if err := netconfig.Apply(tmp, filepath.Join(tmp, "root")); err != nil {
|
if err := netconfig.Apply(tmp, filepath.Join(tmp, "root"), true); err != nil {
|
||||||
t.Fatalf("netconfig.Apply: %v", err)
|
t.Fatalf("netconfig.Apply: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -273,7 +346,7 @@ func TestNetconfig(t *testing.T) {
|
|||||||
// already-configured interfaces, addresses, routes, … (and ensure
|
// already-configured interfaces, addresses, routes, … (and ensure
|
||||||
// nftables rules are replaced, not appendend to).
|
// nftables rules are replaced, not appendend to).
|
||||||
netconfig.DefaultCounterObj = &nftables.CounterObj{Packets: 0, Bytes: 0}
|
netconfig.DefaultCounterObj = &nftables.CounterObj{Packets: 0, Bytes: 0}
|
||||||
if err := netconfig.Apply(tmp, filepath.Join(tmp, "root")); err != nil {
|
if err := netconfig.Apply(tmp, filepath.Join(tmp, "root"), true); err != nil {
|
||||||
t.Fatalf("netconfig.Apply: %v", err)
|
t.Fatalf("netconfig.Apply: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -325,6 +398,9 @@ func TestNetconfig(t *testing.T) {
|
|||||||
if !strings.Contains(string(link), "link/ether 02:73:53:00:b0:aa") {
|
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")
|
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()
|
addrs, err := exec.Command("ip", "-netns", ns, "address", "show", "dev", "uplink0").Output()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -402,11 +478,27 @@ peer: AVU3LodtnFaFnJmMyNNW7cUk4462lqnVULTFkjWYvRo=
|
|||||||
if !upRe.MatchString(string(out)) {
|
if !upRe.MatchString(string(out)) {
|
||||||
t.Errorf("regexp %s does not match %s", upRe, 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*$`)
|
addr6Re := regexp.MustCompile(`(?m)^\s*inet6 fe80::1/64 scope link\s*$`)
|
||||||
if !addr6Re.MatchString(string(out)) {
|
if !addr6Re.MatchString(string(out)) {
|
||||||
t.Errorf("regexp %s does not match %s", addr6Re, 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{
|
opts := []cmp.Option{
|
||||||
@ -453,6 +545,131 @@ 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) {
|
func ipLines(args ...string) ([]string, error) {
|
||||||
cmd := exec.Command("ip", args...)
|
cmd := exec.Command("ip", args...)
|
||||||
out, err := cmd.Output()
|
out, err := cmd.Output()
|
||||||
@ -466,3 +683,124 @@ func ipLines(args ...string) ([]string, error) {
|
|||||||
|
|
||||||
return strings.Split(strings.TrimSpace(outstr), "\n"), nil
|
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,9 +23,10 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"slices"
|
||||||
)
|
)
|
||||||
|
|
||||||
func Archive(w io.Writer, dir string) error {
|
func Archive(w io.Writer, dir string, excludes []string) error {
|
||||||
gw, err := gzip.NewWriterLevel(w, gzip.BestSpeed)
|
gw, err := gzip.NewWriterLevel(w, gzip.BestSpeed)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -46,6 +47,9 @@ func Archive(w io.Writer, dir string) error {
|
|||||||
if path == dir {
|
if path == dir {
|
||||||
return nil // skip root
|
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)
|
rel, err := filepath.Rel(dir, path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -58,7 +62,7 @@ func Archive(w io.Writer, dir string) error {
|
|||||||
if err := tw.WriteHeader(hdr); err != nil {
|
if err := tw.WriteHeader(hdr); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if !info.Mode().IsDir() {
|
if !info.Mode().IsDir() && !slices.Contains(excludes, path) {
|
||||||
b, err := ioutil.ReadFile(path)
|
b, err := ioutil.ReadFile(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
|
@ -25,7 +25,7 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/gopacket/layers"
|
"github.com/google/gopacket/layers"
|
||||||
"github.com/mdlayher/raw"
|
"github.com/mdlayher/packet"
|
||||||
"github.com/rtr7/dhcp4"
|
"github.com/rtr7/dhcp4"
|
||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
)
|
)
|
||||||
@ -44,6 +44,7 @@ type Client struct {
|
|||||||
|
|
||||||
err error
|
err error
|
||||||
once sync.Once
|
once sync.Once
|
||||||
|
onceErr error
|
||||||
connection net.PacketConn
|
connection net.PacketConn
|
||||||
hardwareAddr net.HardwareAddr
|
hardwareAddr net.HardwareAddr
|
||||||
hostname string
|
hostname string
|
||||||
@ -51,6 +52,8 @@ type Client struct {
|
|||||||
timeNow func() time.Time
|
timeNow func() time.Time
|
||||||
generateXID func() uint32
|
generateXID func() uint32
|
||||||
|
|
||||||
|
timeoutCount int
|
||||||
|
|
||||||
// last DHCPACK packet for renewal/release
|
// last DHCPACK packet for renewal/release
|
||||||
Ack *layers.DHCPv4
|
Ack *layers.DHCPv4
|
||||||
}
|
}
|
||||||
@ -84,23 +87,20 @@ var errNAK = errors.New("received DHCPNAK")
|
|||||||
|
|
||||||
// ObtainOrRenew returns false when encountering a permanent error.
|
// ObtainOrRenew returns false when encountering a permanent error.
|
||||||
func (c *Client) ObtainOrRenew() bool {
|
func (c *Client) ObtainOrRenew() bool {
|
||||||
var onceErr error
|
|
||||||
c.once.Do(func() {
|
c.once.Do(func() {
|
||||||
if c.timeNow == nil {
|
if c.timeNow == nil {
|
||||||
c.timeNow = time.Now
|
c.timeNow = time.Now
|
||||||
}
|
}
|
||||||
if c.connection == nil && c.Interface != nil {
|
if c.connection == nil && c.Interface != nil {
|
||||||
conn, err := raw.ListenPacket(c.Interface, syscall.ETH_P_IP, &raw.Config{
|
conn, err := packet.Listen(c.Interface, packet.Datagram, syscall.ETH_P_IP, nil)
|
||||||
LinuxSockDGRAM: true,
|
|
||||||
})
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
onceErr = err
|
c.onceErr = err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.connection = conn
|
c.connection = conn
|
||||||
}
|
}
|
||||||
if c.connection == nil && c.Interface == nil {
|
if c.connection == nil && c.Interface == nil {
|
||||||
onceErr = fmt.Errorf("c.Interface is nil")
|
c.onceErr = fmt.Errorf("c.Interface is nil")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if c.hardwareAddr == nil && c.HWAddr != nil {
|
if c.hardwareAddr == nil && c.HWAddr != nil {
|
||||||
@ -115,21 +115,32 @@ func (c *Client) ObtainOrRenew() bool {
|
|||||||
if c.hostname == "" {
|
if c.hostname == "" {
|
||||||
var utsname unix.Utsname
|
var utsname unix.Utsname
|
||||||
if err := unix.Uname(&utsname); err != nil {
|
if err := unix.Uname(&utsname); err != nil {
|
||||||
onceErr = err
|
c.onceErr = err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
c.hostname = string(utsname.Nodename[:bytes.IndexByte(utsname.Nodename[:], 0)])
|
c.hostname = string(utsname.Nodename[:bytes.IndexByte(utsname.Nodename[:], 0)])
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
if onceErr != nil {
|
if c.onceErr != nil {
|
||||||
c.err = onceErr
|
c.err = c.onceErr
|
||||||
return false // permanent error
|
return false // permanent error
|
||||||
}
|
}
|
||||||
c.err = nil // clear previous error
|
c.err = nil // clear previous error
|
||||||
ack, err := c.dhcpRequest()
|
ack, err := c.dhcpRequest()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errno, ok := err.(syscall.Errno); ok && errno == syscall.EAGAIN {
|
if errno, ok := err.(syscall.Errno); ok && errno == syscall.EAGAIN {
|
||||||
c.err = fmt.Errorf("DHCP: timeout (server(s) unreachable)")
|
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
|
||||||
|
}
|
||||||
return true // temporary error
|
return true // temporary error
|
||||||
}
|
}
|
||||||
if err == errNAK {
|
if err == errNAK {
|
||||||
@ -154,6 +165,7 @@ func (c *Client) ObtainOrRenew() bool {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
c.cfg.RenewAfter = c.timeNow().Add(lease.RenewalTime)
|
c.cfg.RenewAfter = c.timeNow().Add(lease.RenewalTime)
|
||||||
|
c.timeoutCount = 0
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -18,6 +18,7 @@ package dhcp4d
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"math/rand"
|
"math/rand"
|
||||||
"net"
|
"net"
|
||||||
@ -32,22 +33,28 @@ import (
|
|||||||
"github.com/google/gopacket"
|
"github.com/google/gopacket"
|
||||||
"github.com/google/gopacket/layers"
|
"github.com/google/gopacket/layers"
|
||||||
"github.com/krolaw/dhcp4"
|
"github.com/krolaw/dhcp4"
|
||||||
"github.com/mdlayher/raw"
|
"github.com/mdlayher/packet"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Lease struct {
|
type Lease struct {
|
||||||
Num int `json:"num"` // relative to Handler.start
|
Num int `json:"num"` // relative to Handler.start
|
||||||
Addr net.IP `json:"addr"`
|
Addr net.IP `json:"addr"` // subnet.start+Num
|
||||||
HardwareAddr string `json:"hardware_addr"`
|
HardwareAddr string `json:"hardware_addr"`
|
||||||
Hostname string `json:"hostname"`
|
Hostname string `json:"hostname"`
|
||||||
HostnameOverride string `json:"hostname_override"`
|
HostnameOverride string `json:"hostname_override"`
|
||||||
Expiry time.Time `json:"expiry"`
|
Expiry time.Time `json:"expiry"`
|
||||||
|
VendorIdentifier string `json:"vendor"`
|
||||||
|
LastACK time.Time `json:"last_ack"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (l *Lease) Expired(at time.Time) bool {
|
func (l *Lease) Expired(at time.Time) bool {
|
||||||
return !l.Expiry.IsZero() && at.After(l.Expiry)
|
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 {
|
type Handler struct {
|
||||||
serverIP net.IP
|
serverIP net.IP
|
||||||
start net.IP // first IP address to hand out
|
start net.IP // first IP address to hand out
|
||||||
@ -67,7 +74,7 @@ type Handler struct {
|
|||||||
leasesIP map[int]*Lease
|
leasesIP map[int]*Lease
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHandler(dir string, iface *net.Interface, ifaceName string, conn net.PacketConn) (*Handler, error) {
|
func NewHandler(dir string, iface *net.Interface, ifaceName string, conn net.PacketConn, options dhcp4.Options) (*Handler, error) {
|
||||||
serverIP, err := netconfig.LinkAddress(dir, ifaceName)
|
serverIP, err := netconfig.LinkAddress(dir, ifaceName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@ -79,15 +86,29 @@ func NewHandler(dir string, iface *net.Interface, ifaceName string, conn net.Pac
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if conn == nil {
|
if conn == nil {
|
||||||
conn, err = raw.ListenPacket(iface, syscall.ETH_P_ALL, nil)
|
conn, err = packet.Listen(iface, packet.Raw, syscall.ETH_P_ALL, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
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()
|
serverIP = serverIP.To4()
|
||||||
start := make(net.IP, len(serverIP))
|
start := make(net.IP, len(serverIP))
|
||||||
copy(start, serverIP)
|
copy(start, serverIP)
|
||||||
start[len(start)-1] += 1
|
start[len(start)-1]++
|
||||||
return &Handler{
|
return &Handler{
|
||||||
rawConn: conn,
|
rawConn: conn,
|
||||||
iface: iface,
|
iface: iface,
|
||||||
@ -96,18 +117,18 @@ func NewHandler(dir string, iface *net.Interface, ifaceName string, conn net.Pac
|
|||||||
serverIP: serverIP,
|
serverIP: serverIP,
|
||||||
start: start,
|
start: start,
|
||||||
leaseRange: 230,
|
leaseRange: 230,
|
||||||
LeasePeriod: 20 * time.Minute,
|
LeasePeriod: leasePeriod,
|
||||||
options: dhcp4.Options{
|
options: 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,
|
timeNow: time.Now,
|
||||||
}, nil
|
}, 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
|
// SetLeases overwrites the leases database with the specified leases, typically
|
||||||
// loaded from persistent storage. There is no locking, so SetLeases must be
|
// loaded from persistent storage. There is no locking, so SetLeases must be
|
||||||
// called before Serve.
|
// called before Serve.
|
||||||
@ -117,6 +138,9 @@ func (h *Handler) SetLeases(leases []*Lease) {
|
|||||||
h.leasesHW = make(map[string]int)
|
h.leasesHW = make(map[string]int)
|
||||||
h.leasesIP = make(map[int]*Lease)
|
h.leasesIP = make(map[int]*Lease)
|
||||||
for _, l := range leases {
|
for _, l := range leases {
|
||||||
|
if l.LastACK.IsZero() {
|
||||||
|
l.LastACK = l.Expiry
|
||||||
|
}
|
||||||
h.leasesHW[l.HardwareAddr] = l.Num
|
h.leasesHW[l.HardwareAddr] = l.Num
|
||||||
h.leasesIP[l.Num] = l
|
h.leasesIP[l.Num] = l
|
||||||
}
|
}
|
||||||
@ -133,14 +157,18 @@ func (h *Handler) callLeasesLocked(lease *Lease) {
|
|||||||
h.Leases(leases, lease)
|
h.Leases(leases, lease)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) SetHostname(hwaddr, hostname string) {
|
func (h *Handler) SetHostname(hwaddr, hostname string) error {
|
||||||
h.leasesMu.Lock()
|
h.leasesMu.Lock()
|
||||||
defer h.leasesMu.Unlock()
|
defer h.leasesMu.Unlock()
|
||||||
leaseNum := h.leasesHW[hwaddr]
|
leaseNum := h.leasesHW[hwaddr]
|
||||||
lease := h.leasesIP[leaseNum]
|
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.Hostname = hostname
|
||||||
lease.HostnameOverride = hostname
|
lease.HostnameOverride = hostname
|
||||||
h.callLeasesLocked(lease)
|
h.callLeasesLocked(lease)
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) findLease() int {
|
func (h *Handler) findLease() int {
|
||||||
@ -168,7 +196,7 @@ func (h *Handler) canLease(reqIP net.IP, hwaddr string) int {
|
|||||||
}
|
}
|
||||||
|
|
||||||
leaseNum := dhcp4.IPRange(h.start, reqIP) - 1
|
leaseNum := dhcp4.IPRange(h.start, reqIP) - 1
|
||||||
if leaseNum < 0 || leaseNum >= h.leaseRange {
|
if leaseNum < 0 {
|
||||||
return -1
|
return -1
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -176,6 +204,10 @@ func (h *Handler) canLease(reqIP net.IP, hwaddr string) int {
|
|||||||
defer h.leasesMu.Unlock()
|
defer h.leasesMu.Unlock()
|
||||||
l, ok := h.leasesIP[leaseNum]
|
l, ok := h.leasesIP[leaseNum]
|
||||||
if !ok {
|
if !ok {
|
||||||
|
if leaseNum >= h.leaseRange {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
return leaseNum // lease available
|
return leaseNum // lease available
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -183,6 +215,10 @@ func (h *Handler) canLease(reqIP net.IP, hwaddr string) int {
|
|||||||
return leaseNum // lease already owned by requestor
|
return leaseNum // lease already owned by requestor
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if leaseNum >= h.leaseRange {
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
if l.Expired(h.timeNow()) {
|
if l.Expired(h.timeNow()) {
|
||||||
return leaseNum // lease expired
|
return leaseNum // lease expired
|
||||||
}
|
}
|
||||||
@ -232,7 +268,7 @@ func (h *Handler) ServeDHCP(p dhcp4.Packet, msgType dhcp4.MessageType, options d
|
|||||||
udp,
|
udp,
|
||||||
gopacket.Payload(reply))
|
gopacket.Payload(reply))
|
||||||
|
|
||||||
if _, err := h.rawConn.WriteTo(buf.Bytes(), &raw.Addr{destMAC}); err != nil {
|
if _, err := h.rawConn.WriteTo(buf.Bytes(), &packet.Addr{HardwareAddr: destMAC}); err != nil {
|
||||||
log.Printf("WriteTo: %v", err)
|
log.Printf("WriteTo: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -284,18 +320,18 @@ func (h *Handler) serveDHCP(p dhcp4.Packet, msgType dhcp4.MessageType, options d
|
|||||||
// try to offer the requested IP, if any and available
|
// try to offer the requested IP, if any and available
|
||||||
if !reqIP.To4().Equal(net.IPv4zero) {
|
if !reqIP.To4().Equal(net.IPv4zero) {
|
||||||
free = h.canLease(reqIP, hwAddr)
|
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
|
// offer previous lease for this HardwareAddr, if any
|
||||||
if lease, ok := h.leaseHW(hwAddr); ok && !lease.Expired(h.timeNow()) {
|
if lease, ok := h.leaseHW(hwAddr); ok && !lease.Expired(h.timeNow()) {
|
||||||
free = lease.Num
|
free = lease.Num
|
||||||
//log.Printf("h.leasesHW[%s] = %d", hwAddr, free)
|
// log.Printf("h.leasesHW[%s] = %d", hwAddr, free)
|
||||||
}
|
}
|
||||||
|
|
||||||
if free == -1 {
|
if free == -1 {
|
||||||
free = h.findLease()
|
free = h.findLease()
|
||||||
//log.Printf("findLease = %d", free)
|
// log.Printf("findLease = %d", free)
|
||||||
}
|
}
|
||||||
|
|
||||||
if free == -1 {
|
if free == -1 {
|
||||||
@ -318,13 +354,15 @@ func (h *Handler) serveDHCP(p dhcp4.Packet, msgType dhcp4.MessageType, options d
|
|||||||
if leaseNum == -1 {
|
if leaseNum == -1 {
|
||||||
return dhcp4.ReplyPacket(p, dhcp4.NAK, h.serverIP, nil, 0, nil)
|
return dhcp4.ReplyPacket(p, dhcp4.NAK, h.serverIP, nil, 0, nil)
|
||||||
}
|
}
|
||||||
|
now := h.timeNow()
|
||||||
lease := &Lease{
|
lease := &Lease{
|
||||||
Num: leaseNum,
|
Num: leaseNum,
|
||||||
Addr: make([]byte, 4),
|
Addr: make([]byte, 4),
|
||||||
HardwareAddr: hwAddr,
|
HardwareAddr: hwAddr,
|
||||||
Expiry: h.timeNow().Add(h.leasePeriodForDevice(hwAddr)),
|
Expiry: now.Add(h.leasePeriodForDevice(hwAddr)),
|
||||||
Hostname: string(options[dhcp4.OptionHostName]),
|
Hostname: string(options[dhcp4.OptionHostName]),
|
||||||
|
VendorIdentifier: string(bytes.ToValidUTF8(bytes.ReplaceAll(options[dhcp4.OptionVendorClassIdentifier], []byte{0}, []byte{}), []byte{})),
|
||||||
|
LastACK: h.timeNow(),
|
||||||
}
|
}
|
||||||
copy(lease.Addr, reqIP.To4())
|
copy(lease.Addr, reqIP.To4())
|
||||||
|
|
||||||
@ -389,3 +427,78 @@ func (h *Handler) expireLease(hwAddr string) bool {
|
|||||||
l.Expiry = time.Now()
|
l.Expiry = time.Now()
|
||||||
return true
|
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])
|
return dhcp4.MessageType(opts[dhcp4.OptionDHCPMessageType][0])
|
||||||
}
|
}
|
||||||
|
|
||||||
func packet(mt dhcp4.MessageType, addr net.IP, hwaddr net.HardwareAddr, opts []dhcp4.Option) dhcp4.Packet {
|
func newPacket(mt dhcp4.MessageType, addr net.IP, hwaddr net.HardwareAddr, opts []dhcp4.Option) dhcp4.Packet {
|
||||||
return dhcp4.RequestPacket(
|
return dhcp4.RequestPacket(
|
||||||
mt,
|
mt,
|
||||||
hwaddr, // MAC address
|
hwaddr, // MAC address
|
||||||
@ -43,15 +43,15 @@ func packet(mt dhcp4.MessageType, addr net.IP, hwaddr net.HardwareAddr, opts []d
|
|||||||
}
|
}
|
||||||
|
|
||||||
func request(addr net.IP, hwaddr net.HardwareAddr, opts ...dhcp4.Option) dhcp4.Packet {
|
func request(addr net.IP, hwaddr net.HardwareAddr, opts ...dhcp4.Option) dhcp4.Packet {
|
||||||
return packet(dhcp4.Request, addr, hwaddr, opts)
|
return newPacket(dhcp4.Request, addr, hwaddr, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
func discover(addr net.IP, hwaddr net.HardwareAddr, opts ...dhcp4.Option) dhcp4.Packet {
|
func discover(addr net.IP, hwaddr net.HardwareAddr, opts ...dhcp4.Option) dhcp4.Packet {
|
||||||
return packet(dhcp4.Discover, addr, hwaddr, opts)
|
return newPacket(dhcp4.Discover, addr, hwaddr, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
func decline(addr net.IP, hwaddr net.HardwareAddr, opts ...dhcp4.Option) dhcp4.Packet {
|
func decline(addr net.IP, hwaddr net.HardwareAddr, opts ...dhcp4.Option) dhcp4.Packet {
|
||||||
return packet(dhcp4.Decline, addr, hwaddr, opts)
|
return newPacket(dhcp4.Decline, addr, hwaddr, opts)
|
||||||
}
|
}
|
||||||
|
|
||||||
const goldenInterfaces = `
|
const goldenInterfaces = `
|
||||||
@ -95,6 +95,7 @@ func testHandler(t *testing.T) (_ *Handler, cleanup func()) {
|
|||||||
},
|
},
|
||||||
"lan0",
|
"lan0",
|
||||||
&noopSink{},
|
&noopSink{},
|
||||||
|
nil,
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
@ -173,7 +174,7 @@ func TestPreferredAddress(t *testing.T) {
|
|||||||
})
|
})
|
||||||
|
|
||||||
t.Run("requested option", func(t *testing.T) {
|
t.Run("requested option", func(t *testing.T) {
|
||||||
//p := request(net.IPv4zero, hardwareAddr)
|
// p := request(net.IPv4zero, hardwareAddr)
|
||||||
p := dhcp4.RequestPacket(
|
p := dhcp4.RequestPacket(
|
||||||
dhcp4.Discover,
|
dhcp4.Discover,
|
||||||
hardwareAddr, // MAC address
|
hardwareAddr, // MAC address
|
||||||
@ -215,7 +216,6 @@ func TestPoolBoundaries(t *testing.T) {
|
|||||||
t.Errorf("DHCPREQUEST resulted in unexpected message type: got %v, want %v", got, want)
|
t.Errorf("DHCPREQUEST resulted in unexpected message type: got %v, want %v", got, want)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestPreviousLease(t *testing.T) {
|
func TestPreviousLease(t *testing.T) {
|
||||||
@ -465,7 +465,7 @@ func TestMinimumLeaseTime(t *testing.T) {
|
|||||||
handler, cleanup := testHandler(t)
|
handler, cleanup := testHandler(t)
|
||||||
defer cleanup()
|
defer cleanup()
|
||||||
|
|
||||||
var addr = net.IP{192, 168, 42, 23}
|
addr := net.IP{192, 168, 42, 23}
|
||||||
|
|
||||||
for _, tt := range []struct {
|
for _, tt := range []struct {
|
||||||
hwaddr net.HardwareAddr
|
hwaddr net.HardwareAddr
|
||||||
|
@ -266,6 +266,14 @@ func (c *Client) ObtainOrRenew() bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
c.advertise = advertise
|
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)
|
_, reply, err := c.request(advertise)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
c.err = err
|
c.err = err
|
||||||
|
@ -73,6 +73,7 @@ func (d *ping4gw) Evaluate() (string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
defer p.Close()
|
||||||
rtt, err := p.Ping(addr, timeout)
|
rtt, err := p.Ping(addr, timeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@ -115,6 +116,7 @@ func (d *ping4) Evaluate() (string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
defer p.Close()
|
||||||
rtt, err := p.Ping(addr, timeout)
|
rtt, err := p.Ping(addr, timeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
@ -177,6 +179,7 @@ func (d *ping6gw) Evaluate() (string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("ping.New(::): %v", err)
|
return "", fmt.Errorf("ping.New(::): %v", err)
|
||||||
}
|
}
|
||||||
|
defer p.Close()
|
||||||
rtt, err := p.Ping(addr, timeout)
|
rtt, err := p.Ping(addr, timeout)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("ping6(%v, %v): %v", addr, timeout, err)
|
return "", fmt.Errorf("ping6(%v, %v): %v", addr, timeout, err)
|
||||||
@ -251,6 +254,7 @@ func (d *ping6) Evaluate() (string, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
|
defer p.Close()
|
||||||
ctx, canc := context.WithTimeout(context.Background(), timeout)
|
ctx, canc := context.WithTimeout(context.Background(), timeout)
|
||||||
defer canc()
|
defer canc()
|
||||||
if strings.HasPrefix(addr.String(), "ff02::") {
|
if strings.HasPrefix(addr.String(), "ff02::") {
|
||||||
@ -274,6 +278,11 @@ func (d *ping6) Evaluate() (string, error) {
|
|||||||
if localAddr[reply.Address.String()] {
|
if localAddr[reply.Address.String()] {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
go func() {
|
||||||
|
for range replies {
|
||||||
|
// drain channel
|
||||||
|
}
|
||||||
|
}()
|
||||||
return formatRTT(reply.Duration) + " from " + reply.Address.String(), nil
|
return formatRTT(reply.Duration) + " from " + reply.Address.String(), nil
|
||||||
}
|
}
|
||||||
return "", fmt.Errorf("no responses to %s within %v", addr, timeout)
|
return "", fmt.Errorf("no responses to %s within %v", addr, timeout)
|
||||||
|
@ -53,6 +53,7 @@ func TCP4(addr string) Node {
|
|||||||
|
|
||||||
type tcp6 struct {
|
type tcp6 struct {
|
||||||
children []Node
|
children []Node
|
||||||
|
ifname string
|
||||||
addr string
|
addr string
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -70,7 +71,39 @@ func (d *tcp6) Children() []Node {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (d *tcp6) Evaluate() (string, error) {
|
func (d *tcp6) Evaluate() (string, error) {
|
||||||
conn, err := net.Dial("tcp6", d.addr)
|
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)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", err
|
return "", err
|
||||||
}
|
}
|
||||||
@ -80,6 +113,9 @@ func (d *tcp6) Evaluate() (string, error) {
|
|||||||
|
|
||||||
// TCP6 returns a Node which succeeds when the specified address accepts a TCPv6
|
// TCP6 returns a Node which succeeds when the specified address accepts a TCPv6
|
||||||
// connection.
|
// connection.
|
||||||
func TCP6(addr string) Node {
|
func TCP6(ifname, addr string) Node {
|
||||||
return &tcp6{addr: addr}
|
return &tcp6{
|
||||||
|
ifname: ifname,
|
||||||
|
addr: addr,
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@ -90,14 +90,16 @@ func NewServer(addr, domain string) *Server {
|
|||||||
domain: lcHostname(strings.ToLower(domain)),
|
domain: lcHostname(strings.ToLower(domain)),
|
||||||
upstream: []string{
|
upstream: []string{
|
||||||
// https://developers.google.com/speed/public-dns/docs/using#google_public_dns_ip_addresses
|
// https://developers.google.com/speed/public-dns/docs/using#google_public_dns_ip_addresses
|
||||||
"1.1.1.1:53",
|
"45.90.28.26:53",
|
||||||
"1.0.0.1:53",
|
"45.90.30.26:53",
|
||||||
"2606:4700:4700::1111:53",
|
"[2a07:a8c0::54:f68e]:53",
|
||||||
"2606:4700:4700::1001:53",
|
"[2a07:a8c1::54:f68e]:53",
|
||||||
"8.8.8.8:53",
|
"194.242.2.4:53",
|
||||||
"8.8.4.4:53",
|
"[2a07:e340::4]:52",
|
||||||
"[2001:4860:4860::8888]:53",
|
"94.140.14.14:53",
|
||||||
"[2001:4860:4860::8844]:53",
|
"94.140.15.15:53",
|
||||||
|
"[2a10:50c0::ad1:ff]:53",
|
||||||
|
"[2a10:50c0::ad2:ff]:53",
|
||||||
},
|
},
|
||||||
sometimes: rate.NewLimiter(rate.Every(1*time.Second), 1), // at most once per second
|
sometimes: rate.NewLimiter(rate.Every(1*time.Second), 1), // at most once per second
|
||||||
hostname: hostname,
|
hostname: hostname,
|
||||||
@ -143,6 +145,11 @@ func NewServer(addr, domain string) *Server {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) initHostsLocked() {
|
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.hostsByName = make(map[lcHostname]string)
|
||||||
s.hostsByIP = make(map[string]string)
|
s.hostsByIP = make(map[string]string)
|
||||||
s.subnames[s.domain] = make(map[lcHostname]IP)
|
s.subnames[s.domain] = make(map[lcHostname]IP)
|
||||||
@ -172,8 +179,6 @@ func (m measurement) String() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) probeUpstreamLatency() {
|
func (s *Server) probeUpstreamLatency() {
|
||||||
if !s.once {
|
|
||||||
s.once = true
|
|
||||||
upstreams := s.upstreams()
|
upstreams := s.upstreams()
|
||||||
results := make([]measurement, len(upstreams))
|
results := make([]measurement, len(upstreams))
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
@ -201,14 +206,13 @@ func (s *Server) probeUpstreamLatency() {
|
|||||||
sort.Slice(results, func(i, j int) bool {
|
sort.Slice(results, func(i, j int) bool {
|
||||||
return results[i].rtt < results[j].rtt
|
return results[i].rtt < results[j].rtt
|
||||||
})
|
})
|
||||||
log.Printf("probe results: %v %v", s.once, results)
|
log.Printf("probe results: %v", results)
|
||||||
for idx, result := range results {
|
for idx, result := range results {
|
||||||
upstreams[idx] = result.upstream
|
upstreams[idx] = result.upstream
|
||||||
}
|
}
|
||||||
s.upstreamMu.Lock()
|
s.upstreamMu.Lock()
|
||||||
defer s.upstreamMu.Unlock()
|
defer s.upstreamMu.Unlock()
|
||||||
s.upstream = upstreams
|
s.upstream = upstreams
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) hostByName(n lcHostname) (string, bool) {
|
func (s *Server) hostByName(n lcHostname) (string, bool) {
|
||||||
@ -225,10 +229,10 @@ func (s *Server) hostByIP(n string) (string, bool) {
|
|||||||
return r, ok
|
return r, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) subname(hostname, host string) (IP, bool) {
|
func (s *Server) subname(domain, host string) (IP, bool) {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
defer s.mu.Unlock()
|
defer s.mu.Unlock()
|
||||||
r, ok := s.subnames[lcHostname(strings.ToLower(hostname))][lcHostname(strings.ToLower(host))]
|
r, ok := s.subnames[lcHostname(strings.ToLower(domain))][lcHostname(strings.ToLower(host))]
|
||||||
return r, ok
|
return r, ok
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -344,6 +348,15 @@ func (s *Server) SetDNSEntries(dnsEntries []IP) {
|
|||||||
entry.Host = lcHostname(strings.TrimSuffix(dn, "lan")) + s.domain
|
entry.Host = lcHostname(strings.TrimSuffix(dn, "lan")) + s.domain
|
||||||
}
|
}
|
||||||
s.setSubname(entry)
|
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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -480,6 +493,7 @@ func (s *Server) handleInternal(w dns.ResponseWriter, r *dns.Msg) {
|
|||||||
if err == errEmpty {
|
if err == errEmpty {
|
||||||
m := new(dns.Msg)
|
m := new(dns.Msg)
|
||||||
m.SetReply(r)
|
m.SetReply(r)
|
||||||
|
m.RecursionAvailable = true
|
||||||
w.WriteMsg(m)
|
w.WriteMsg(m)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -488,6 +502,7 @@ func (s *Server) handleInternal(w dns.ResponseWriter, r *dns.Msg) {
|
|||||||
if rr != nil {
|
if rr != nil {
|
||||||
m := new(dns.Msg)
|
m := new(dns.Msg)
|
||||||
m.SetReply(r)
|
m.SetReply(r)
|
||||||
|
m.RecursionAvailable = true
|
||||||
m.Answer = append(m.Answer, rr)
|
m.Answer = append(m.Answer, rr)
|
||||||
w.WriteMsg(m)
|
w.WriteMsg(m)
|
||||||
return
|
return
|
||||||
@ -495,6 +510,7 @@ func (s *Server) handleInternal(w dns.ResponseWriter, r *dns.Msg) {
|
|||||||
// Send an authoritative NXDOMAIN for local:
|
// Send an authoritative NXDOMAIN for local:
|
||||||
m := new(dns.Msg)
|
m := new(dns.Msg)
|
||||||
m.SetReply(r)
|
m.SetReply(r)
|
||||||
|
m.RecursionAvailable = true
|
||||||
m.SetRcode(r, dns.RcodeNameError)
|
m.SetRcode(r, dns.RcodeNameError)
|
||||||
w.WriteMsg(m)
|
w.WriteMsg(m)
|
||||||
}
|
}
|
||||||
@ -522,6 +538,7 @@ func (s *Server) handleRequest(w dns.ResponseWriter, r *dns.Msg) {
|
|||||||
|
|
||||||
s.promInc("DNS", r)
|
s.promInc("DNS", r)
|
||||||
|
|
||||||
|
if r.RecursionDesired {
|
||||||
for idx, u := range s.upstreams() {
|
for idx, u := range s.upstreams() {
|
||||||
in, _, err := s.client.Exchange(r, u)
|
in, _, err := s.client.Exchange(r, u)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -530,27 +547,76 @@ func (s *Server) handleRequest(w dns.ResponseWriter, r *dns.Msg) {
|
|||||||
}
|
}
|
||||||
continue // fall back to next-slower upstream
|
continue // fall back to next-slower upstream
|
||||||
}
|
}
|
||||||
|
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)
|
w.WriteMsg(in)
|
||||||
if idx > 0 {
|
if idx > 0 {
|
||||||
// re-order this upstream to the front of s.upstream.
|
// re-order this upstream to the front of s.upstream.
|
||||||
s.upstreamMu.Lock()
|
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.upstream = append(append([]string{u}, s.upstream[:idx]...), s.upstream[idx+1:]...)
|
||||||
|
}
|
||||||
s.upstreamMu.Unlock()
|
s.upstreamMu.Unlock()
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
} 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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
// DNS has no reply for resolving errors
|
// 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) {
|
func (s *Server) resolveSubname(domain string, q dns.Question) (dns.RR, error) {
|
||||||
if q.Qclass != dns.ClassINET {
|
if q.Qclass != dns.ClassINET {
|
||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
ip, ok := s.getSubname(domain, q.Name)
|
||||||
if q.Qtype == dns.TypeA || q.Qtype == dns.TypeAAAA /*|| q.Qtype == dns.TypeMX*/ {
|
if q.Qtype == dns.TypeA || q.Qtype == dns.TypeAAAA /*|| q.Qtype == dns.TypeMX*/ {
|
||||||
name := strings.TrimSuffix(q.Name, ".")
|
if ok {
|
||||||
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 {
|
if q.Qtype == dns.TypeA && ip.IPv4.To4() != nil {
|
||||||
return dns.NewRR(q.Name + " 3600 IN A " + ip.IPv4.String())
|
return dns.NewRR(q.Name + " 3600 IN A " + ip.IPv4.String())
|
||||||
}
|
}
|
||||||
@ -569,19 +635,20 @@ func (s *Server) promInc(label string, r *dns.Msg) {
|
|||||||
s.prom.upstream.WithLabelValues(label).Inc()
|
s.prom.upstream.WithLabelValues(label).Inc()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *Server) subnameHandler(hostname lcHostname) func(w dns.ResponseWriter, r *dns.Msg) {
|
func (s *Server) subnameHandler(domain lcHostname) func(w dns.ResponseWriter, r *dns.Msg) {
|
||||||
return 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
|
if len(r.Question) != 1 { // TODO: answer all questions we can answer
|
||||||
s.promInc("local", r)
|
s.promInc("local", r)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
rr, err := s.resolveSubname(string(hostname), r.Question[0])
|
rr, err := s.resolveSubname(string(domain), r.Question[0])
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.promInc("local", r)
|
s.promInc("local", r)
|
||||||
if err == errEmpty {
|
if err == errEmpty {
|
||||||
m := new(dns.Msg)
|
m := new(dns.Msg)
|
||||||
m.SetReply(r)
|
m.SetReply(r)
|
||||||
|
m.RecursionAvailable = true
|
||||||
w.WriteMsg(m)
|
w.WriteMsg(m)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -591,16 +658,18 @@ func (s *Server) subnameHandler(hostname lcHostname) func(w dns.ResponseWriter,
|
|||||||
s.promInc("local", r)
|
s.promInc("local", r)
|
||||||
m := new(dns.Msg)
|
m := new(dns.Msg)
|
||||||
m.SetReply(r)
|
m.SetReply(r)
|
||||||
|
m.RecursionAvailable = true
|
||||||
m.Answer = append(m.Answer, rr)
|
m.Answer = append(m.Answer, rr)
|
||||||
w.WriteMsg(m)
|
w.WriteMsg(m)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// Send an authoritative NXDOMAIN for local names:
|
// Send an authoritative NXDOMAIN for local names:
|
||||||
if r.Question[0].Qtype == dns.TypePTR || !strings.Contains(strings.TrimSuffix(r.Question[0].Name, "."), ".") || strings.HasSuffix(r.Question[0].Name, ".lan.") {
|
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.") {
|
||||||
s.promInc("local", r)
|
s.promInc("local", r)
|
||||||
m := new(dns.Msg)
|
m := new(dns.Msg)
|
||||||
m.SetReply(r)
|
m.SetReply(r)
|
||||||
|
m.RecursionAvailable = true
|
||||||
m.SetRcode(r, dns.RcodeNameError)
|
m.SetRcode(r, dns.RcodeNameError)
|
||||||
w.WriteMsg(m)
|
w.WriteMsg(m)
|
||||||
return
|
return
|
||||||
|
@ -158,6 +158,28 @@ 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) {
|
func TestDHCP(t *testing.T) {
|
||||||
r := &recorder{}
|
r := &recorder{}
|
||||||
s := NewServer("localhost:0", "lan")
|
s := NewServer("localhost:0", "lan")
|
||||||
@ -620,3 +642,93 @@ 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
|
var updated []libdns.Record
|
||||||
for _, rec := range existing {
|
for _, rec := range existing {
|
||||||
if rec.Name != record.Name || rec.Type != record.Type {
|
if rec.Name+"."+zone != record.Name || rec.Type != record.Type {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -21,6 +21,7 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net"
|
"net"
|
||||||
"os"
|
"os"
|
||||||
|
"os/exec"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
@ -31,7 +32,7 @@ import (
|
|||||||
"github.com/google/nftables"
|
"github.com/google/nftables"
|
||||||
"github.com/google/nftables/binaryutil"
|
"github.com/google/nftables/binaryutil"
|
||||||
"github.com/google/nftables/expr"
|
"github.com/google/nftables/expr"
|
||||||
"github.com/google/renameio"
|
"github.com/mdlayher/ethtool"
|
||||||
"github.com/vishvananda/netlink"
|
"github.com/vishvananda/netlink"
|
||||||
"golang.org/x/sys/unix"
|
"golang.org/x/sys/unix"
|
||||||
|
|
||||||
@ -62,7 +63,7 @@ func subnetMaskSize(mask string) (int, error) {
|
|||||||
return ones, nil
|
return ones, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyDhcp4(dir string) error {
|
func applyDhcp4(dir string, cfg InterfaceConfig) error {
|
||||||
b, err := ioutil.ReadFile(filepath.Join(dir, "dhcp4/wire/lease.json"))
|
b, err := ioutil.ReadFile(filepath.Join(dir, "dhcp4/wire/lease.json"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
@ -75,7 +76,8 @@ func applyDhcp4(dir string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
link, err := netlink.LinkByName("uplink0")
|
const linkName = "uplink0"
|
||||||
|
link, err := netlink.LinkByName(linkName)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -89,7 +91,8 @@ func applyDhcp4(dir string) error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
addr, err := netlink.ParseAddr(fmt.Sprintf("%s/%d", got.ClientIP, subnetSize))
|
gotAddr := fmt.Sprintf("%s/%d", got.ClientIP, subnetSize)
|
||||||
|
addr, err := netlink.ParseAddr(gotAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@ -99,8 +102,24 @@ func applyDhcp4(dir string) error {
|
|||||||
return fmt.Errorf("netlink.NewHandle: %v", err)
|
return fmt.Errorf("netlink.NewHandle: %v", err)
|
||||||
}
|
}
|
||||||
defer h.Delete()
|
defer h.Delete()
|
||||||
|
log.Printf("replacing address %v on %v", addr, linkName)
|
||||||
if err := h.AddrReplace(link, addr); err != nil {
|
if err := h.AddrReplace(link, addr); err != nil {
|
||||||
return fmt.Errorf("AddrReplace(%v): %v", addr, err)
|
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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// from include/uapi/linux/rtnetlink.h
|
// from include/uapi/linux/rtnetlink.h
|
||||||
@ -122,6 +141,51 @@ func applyDhcp4(dir string) error {
|
|||||||
return fmt.Errorf("RouteReplace(router): %v", err)
|
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{
|
if err := h.RouteReplace(&netlink.Route{
|
||||||
LinkIndex: link.Attrs().Index,
|
LinkIndex: link.Attrs().Index,
|
||||||
Dst: &net.IPNet{
|
Dst: &net.IPNet{
|
||||||
@ -134,10 +198,30 @@ func applyDhcp4(dir string) error {
|
|||||||
}); err != nil {
|
}); err != nil {
|
||||||
return fmt.Errorf("RouteReplace(default): %v", err)
|
return fmt.Errorf("RouteReplace(default): %v", err)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return nil
|
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 {
|
func applyDhcp6(dir string) error {
|
||||||
b, err := ioutil.ReadFile(filepath.Join(dir, "dhcp6/wire/lease.json"))
|
b, err := ioutil.ReadFile(filepath.Join(dir, "dhcp6/wire/lease.json"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -176,15 +260,36 @@ func applyDhcp6(dir string) error {
|
|||||||
return nil
|
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 {
|
type InterfaceDetails struct {
|
||||||
HardwareAddr string `json:"hardware_addr"` // e.g. dc:9b:9c:ee:72:fd
|
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
|
SpoofHardwareAddr string `json:"spoof_hardware_addr"` // e.g. dc:9b:9c:ee:72:fd
|
||||||
Name string `json:"name"` // e.g. uplink0, or lan0
|
Name string `json:"name"` // e.g. uplink0, or lan0
|
||||||
Addr string `json:"addr"` // e.g. 192.168.42.1/24
|
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"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type InterfaceConfig struct {
|
type InterfaceConfig struct {
|
||||||
Interfaces []InterfaceDetails `json:"interfaces"`
|
Interfaces []InterfaceDetails `json:"interfaces"`
|
||||||
|
Bridges []BridgeDetails `json:"bridges"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// Interface returns the InterfaceDetails configured for interface ifname in
|
// Interface returns the InterfaceDetails configured for interface ifname in
|
||||||
@ -219,18 +324,159 @@ func LinkAddress(dir, ifname string) (net.IP, error) {
|
|||||||
return ip, err
|
return ip, err
|
||||||
}
|
}
|
||||||
|
|
||||||
func applyInterfaces(dir, root string) error {
|
func applyBridges(cfg *InterfaceConfig) error {
|
||||||
b, err := ioutil.ReadFile(filepath.Join(dir, "interfaces.json"))
|
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 {
|
if err != nil {
|
||||||
if os.IsNotExist(err) {
|
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
|
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()
|
||||||
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
var cfg InterfaceConfig
|
defer cl.Close()
|
||||||
if err := json.Unmarshal(b, &cfg); err != nil {
|
|
||||||
|
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 {
|
||||||
return err
|
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)
|
byName := make(map[string]InterfaceDetails)
|
||||||
byHardwareAddr := make(map[string]InterfaceDetails)
|
byHardwareAddr := make(map[string]InterfaceDetails)
|
||||||
for _, details := range cfg.Interfaces {
|
for _, details := range cfg.Interfaces {
|
||||||
@ -240,6 +486,11 @@ func applyInterfaces(dir, root string) error {
|
|||||||
}
|
}
|
||||||
byName[details.Name] = details
|
byName[details.Name] = details
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if err := applyBridges(&cfg); err != nil {
|
||||||
|
log.Printf("applyBridges: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
links, err := netlink.LinkList()
|
links, err := netlink.LinkList()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@ -261,6 +512,9 @@ func applyInterfaces(dir, root string) error {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
details, ok = byHardwareAddr[addr]
|
details, ok = byHardwareAddr[addr]
|
||||||
|
if !ok {
|
||||||
|
details, ok = byName[attr.Name]
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if !ok {
|
if !ok {
|
||||||
log.Printf("no config for interface %s/%s", attr.Name, addr)
|
log.Printf("no config for interface %s/%s", attr.Name, addr)
|
||||||
@ -274,6 +528,12 @@ func applyInterfaces(dir, root string) error {
|
|||||||
attr.Name = details.Name
|
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 != "" {
|
if spoof := details.SpoofHardwareAddr; spoof != "" {
|
||||||
hwaddr, err := net.ParseMAC(spoof)
|
hwaddr, err := net.ParseMAC(spoof)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -284,6 +544,11 @@ func applyInterfaces(dir, root string) 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 {
|
if attr.OperState != netlink.OperUp {
|
||||||
// Set the interface to up, which is required by all other configuration.
|
// Set the interface to up, which is required by all other configuration.
|
||||||
if err := netlink.LinkSetUp(l); err != nil {
|
if err := netlink.LinkSetUp(l); err != nil {
|
||||||
@ -302,15 +567,42 @@ func applyInterfaces(dir, root string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if details.Name == "lan0" {
|
if details.Name == "lan0" {
|
||||||
b := []byte("nameserver " + addr.IP.String() + "\n")
|
// Use dnsd for the system's own DNS resolution.
|
||||||
fn := filepath.Join(root, "tmp", "resolv.conf")
|
resolvConf := "nameserver " + addr.IP.String() + "\n"
|
||||||
if err := os.Remove(fn); err != nil && !os.IsNotExist(err) {
|
if err := createResolvConfIfMissing(root, resolvConf); err != nil {
|
||||||
return 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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
@ -322,7 +614,76 @@ func nfifname(n string) []byte {
|
|||||||
return b
|
return b
|
||||||
}
|
}
|
||||||
|
|
||||||
func portForwardExpr(ifname string, proto uint8, portMin, portMax uint16, dest net.IP, dportMin, dportMax uint16) []expr.Any {
|
// 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 {
|
||||||
var cmp []expr.Any
|
var cmp []expr.Any
|
||||||
if portMin == portMax {
|
if portMin == portMax {
|
||||||
cmp = []expr.Any{
|
cmp = []expr.Any{
|
||||||
@ -349,16 +710,7 @@ func portForwardExpr(ifname string, proto uint8, portMin, portMax uint16, dest n
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
ex := []expr.Any{
|
ex := append(matchUplinkIP(lan0ip),
|
||||||
// [ 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 ]
|
// [ meta load l4proto => reg 1 ]
|
||||||
&expr.Meta{Key: expr.MetaKeyL4PROTO, Register: 1},
|
&expr.Meta{Key: expr.MetaKeyL4PROTO, Register: 1},
|
||||||
// [ cmp eq reg 1 0x00000006 ]
|
// [ cmp eq reg 1 0x00000006 ]
|
||||||
@ -374,8 +726,7 @@ func portForwardExpr(ifname string, proto uint8, portMin, portMax uint16, dest n
|
|||||||
Base: expr.PayloadBaseTransportHeader,
|
Base: expr.PayloadBaseTransportHeader,
|
||||||
Offset: 2, // TODO
|
Offset: 2, // TODO
|
||||||
Len: 2, // TODO
|
Len: 2, // TODO
|
||||||
},
|
})
|
||||||
}
|
|
||||||
ex = append(ex, cmp...)
|
ex = append(ex, cmp...)
|
||||||
ex = append(ex,
|
ex = append(ex,
|
||||||
// [ immediate reg 1 0x0217a8c0 ]
|
// [ immediate reg 1 0x0217a8c0 ]
|
||||||
@ -469,6 +820,15 @@ func applyPortForwardings(dir, ifname string, c *nftables.Conn, nat *nftables.Ta
|
|||||||
return err
|
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 _, fw := range cfg.Forwardings {
|
||||||
for _, proto := range strings.Split(fw.Proto, ",") {
|
for _, proto := range strings.Split(fw.Proto, ",") {
|
||||||
var p uint8
|
var p uint8
|
||||||
@ -493,7 +853,7 @@ func applyPortForwardings(dir, ifname string, c *nftables.Conn, nat *nftables.Ta
|
|||||||
c.AddRule(&nftables.Rule{
|
c.AddRule(&nftables.Rule{
|
||||||
Table: nat,
|
Table: nat,
|
||||||
Chain: prerouting,
|
Chain: prerouting,
|
||||||
Exprs: portForwardExpr(ifname, p, min, max, net.ParseIP(fw.DestAddr), dmin, dmax),
|
Exprs: portForwardExpr(lan0ip, p, min, max, net.ParseIP(fw.DestAddr), dmin, dmax),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -504,35 +864,13 @@ func applyPortForwardings(dir, ifname string, c *nftables.Conn, nat *nftables.Ta
|
|||||||
var DefaultCounterObj = &nftables.CounterObj{}
|
var DefaultCounterObj = &nftables.CounterObj{}
|
||||||
|
|
||||||
func getCounterObj(c *nftables.Conn, o *nftables.CounterObj) *nftables.CounterObj {
|
func getCounterObj(c *nftables.Conn, o *nftables.CounterObj) *nftables.CounterObj {
|
||||||
objs, err := c.GetObj(o)
|
obj, err := c.GetObject(o)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
o.Bytes = DefaultCounterObj.Bytes
|
o.Bytes = DefaultCounterObj.Bytes
|
||||||
o.Packets = DefaultCounterObj.Packets
|
o.Packets = DefaultCounterObj.Packets
|
||||||
return o
|
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
|
return co
|
||||||
}
|
}
|
||||||
o.Bytes = DefaultCounterObj.Bytes
|
o.Bytes = DefaultCounterObj.Bytes
|
||||||
@ -540,14 +878,99 @@ func getCounterObj(c *nftables.Conn, o *nftables.CounterObj) *nftables.CounterOb
|
|||||||
return o
|
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 {
|
func applyFirewall(dir, ifname string) error {
|
||||||
c := &nftables.Conn{}
|
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()
|
c.FlushRuleset()
|
||||||
|
|
||||||
nat := c.AddTable(&nftables.Table{
|
nat := c.AddTable(&nftables.Table{
|
||||||
Family: nftables.TableFamilyIPv4,
|
Family: nftables.TableFamilyIPv4,
|
||||||
Name: "nat",
|
Name: "nat-gokrazy",
|
||||||
|
})
|
||||||
|
|
||||||
|
pf := c.AddChain(&nftables.Chain{
|
||||||
|
Name: pfChain,
|
||||||
|
Table: nat,
|
||||||
|
Type: nftables.ChainTypeNAT,
|
||||||
})
|
})
|
||||||
|
|
||||||
prerouting := c.AddChain(&nftables.Chain{
|
prerouting := c.AddChain(&nftables.Chain{
|
||||||
@ -558,6 +981,17 @@ func applyFirewall(dir, ifname string) error {
|
|||||||
Type: nftables.ChainTypeNAT,
|
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{
|
postrouting := c.AddChain(&nftables.Chain{
|
||||||
Name: "postrouting",
|
Name: "postrouting",
|
||||||
Hooknum: nftables.ChainHookPostrouting,
|
Hooknum: nftables.ChainHookPostrouting,
|
||||||
@ -583,18 +1017,24 @@ func applyFirewall(dir, ifname string) error {
|
|||||||
},
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
if err := applyPortForwardings(dir, ifname, c, nat, prerouting); err != nil {
|
c.AddRule(&nftables.Rule{
|
||||||
|
Table: nat,
|
||||||
|
Chain: postrouting,
|
||||||
|
Exprs: hairpinDNAT(),
|
||||||
|
})
|
||||||
|
|
||||||
|
if err := applyPortForwardings(dir, ifname, c, nat, pf); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
filter4 := c.AddTable(&nftables.Table{
|
filter4 := c.AddTable(&nftables.Table{
|
||||||
Family: nftables.TableFamilyIPv4,
|
Family: nftables.TableFamilyIPv4,
|
||||||
Name: "filter",
|
Name: "filter-gokrazy",
|
||||||
})
|
})
|
||||||
|
|
||||||
filter6 := c.AddTable(&nftables.Table{
|
filter6 := c.AddTable(&nftables.Table{
|
||||||
Family: nftables.TableFamilyIPv6,
|
Family: nftables.TableFamilyIPv6,
|
||||||
Name: "filter",
|
Name: "filter-gokrazy",
|
||||||
})
|
})
|
||||||
|
|
||||||
for _, filter := range []*nftables.Table{filter4, filter6} {
|
for _, filter := range []*nftables.Table{filter4, filter6} {
|
||||||
@ -692,6 +1132,56 @@ 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()
|
return c.Flush()
|
||||||
@ -716,6 +1206,8 @@ func applySysctl(ifname string) error {
|
|||||||
sysctls := []string{
|
sysctls := []string{
|
||||||
"net.ipv4.ip_forward=1",
|
"net.ipv4.ip_forward=1",
|
||||||
"net.ipv6.conf.all.forwarding=1",
|
"net.ipv6.conf.all.forwarding=1",
|
||||||
|
"net.ipv4.icmp_ratelimit=0",
|
||||||
|
"net.ipv6.icmp.ratelimit=0",
|
||||||
}
|
}
|
||||||
if ifname != "" {
|
if ifname != "" {
|
||||||
sysctls = append(sysctls, "net.ipv6.conf."+ifname+".accept_ra=2")
|
sysctls = append(sysctls, "net.ipv6.conf."+ifname+".accept_ra=2")
|
||||||
@ -733,11 +1225,21 @@ func applySysctl(ifname string) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Apply(dir, root string, firewall bool) 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 into two parts: delay the up until later
|
// TODO: split apply into two parts: delay the up until later
|
||||||
if err := applyInterfaces(dir, root); err != nil {
|
if err := applyInterfaces(dir, root, cfg); err != nil {
|
||||||
return fmt.Errorf("interfaces: %v", err)
|
return fmt.Errorf("interfaces: %v", err)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
var errors []error
|
var errors []error
|
||||||
appendError := func(err error) {
|
appendError := func(err error) {
|
||||||
@ -745,7 +1247,7 @@ func Apply(dir, root string, firewall bool) error {
|
|||||||
log.Println(err)
|
log.Println(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := applyDhcp4(dir); err != nil {
|
if err := applyDhcp4(dir, cfg); err != nil {
|
||||||
appendError(fmt.Errorf("dhcp4: %v", err))
|
appendError(fmt.Errorf("dhcp4: %v", err))
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -778,6 +1280,24 @@ func Apply(dir, root string, firewall bool) error {
|
|||||||
if err := applyFirewall(dir, ifname); err != nil {
|
if err := applyFirewall(dir, ifname); err != nil {
|
||||||
appendError(fmt.Errorf("firewall: %v", err))
|
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 {
|
if err := applyWireGuard(dir); err != nil {
|
||||||
@ -789,3 +1309,12 @@ func Apply(dir, root string, firewall bool) error {
|
|||||||
}
|
}
|
||||||
return nil
|
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
|
return err
|
||||||
}
|
}
|
||||||
if !strings.HasPrefix(string(b), name) {
|
if !strings.Contains(string(b), name) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
pid, _ := strconv.Atoi(fi.Name()) // already verified to be numeric
|
pid, _ := strconv.Atoi(fi.Name()) // already verified to be numeric
|
||||||
|
@ -18,6 +18,8 @@ package radvd
|
|||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
"net"
|
"net"
|
||||||
|
"net/netip"
|
||||||
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@ -92,6 +94,10 @@ func (s *Server) Serve(ifname string, conn net.PacketConn) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
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?
|
// TODO: isn’t this guaranteed by the filter above?
|
||||||
if n == 0 ||
|
if n == 0 ||
|
||||||
ipv6.ICMPType(buf[0]) != ipv6.ICMPTypeRouterSolicitation {
|
ipv6.ICMPType(buf[0]) != ipv6.ICMPTypeRouterSolicitation {
|
||||||
@ -144,21 +150,21 @@ func (s *Server) sendAdvertisement(addr net.Addr) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
var linkLocal net.IP
|
var linkLocal netip.Addr
|
||||||
for _, addr := range addrs {
|
for _, addr := range addrs {
|
||||||
ipnet, ok := addr.(*net.IPNet)
|
ipnet, ok := addr.(*net.IPNet)
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if ipv6LinkLocal.Contains(ipnet.IP) {
|
if ipv6LinkLocal.Contains(ipnet.IP) {
|
||||||
linkLocal = ipnet.IP
|
linkLocal, _ = netip.AddrFromSlice(ipnet.IP)
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if !linkLocal.Equal(net.IPv6zero) {
|
if linkLocal.IsValid() && !linkLocal.IsUnspecified() {
|
||||||
options = append(options, &ndp.RecursiveDNSServer{
|
options = append(options, &ndp.RecursiveDNSServer{
|
||||||
Lifetime: 30 * time.Minute,
|
Lifetime: 30 * time.Minute,
|
||||||
Servers: []net.IP{linkLocal},
|
Servers: []netip.Addr{linkLocal},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -170,13 +176,14 @@ func (s *Server) sendAdvertisement(addr net.Addr) error {
|
|||||||
ones = 64
|
ones = 64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addr, _ := netip.AddrFromSlice(prefix.IP)
|
||||||
options = append(options, &ndp.PrefixInformation{
|
options = append(options, &ndp.PrefixInformation{
|
||||||
PrefixLength: uint8(ones),
|
PrefixLength: uint8(ones),
|
||||||
OnLink: true,
|
OnLink: true,
|
||||||
AutonomousAddressConfiguration: true,
|
AutonomousAddressConfiguration: true,
|
||||||
ValidLifetime: 2 * time.Hour,
|
ValidLifetime: 2 * time.Hour,
|
||||||
PreferredLifetime: 30 * time.Minute,
|
PreferredLifetime: 30 * time.Minute,
|
||||||
Prefix: prefix.IP,
|
Prefix: addr,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -23,12 +23,36 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
)
|
)
|
||||||
|
|
||||||
// NewConsole returns a logger which returns to /dev/console and os.Stderr.
|
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).
|
||||||
func NewConsole() *log.Logger {
|
func NewConsole() *log.Logger {
|
||||||
var w io.Writer
|
w := ioutil.Discard
|
||||||
w, err := os.OpenFile("/dev/console", os.O_RDWR, 0600)
|
if console, err := os.OpenFile("/dev/console", os.O_RDWR, 0600); err == nil {
|
||||||
if err != nil {
|
ch := make(chan string, 1)
|
||||||
w = ioutil.Discard
|
go func() {
|
||||||
|
for buf := range ch {
|
||||||
|
console.Write([]byte(buf))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
w = &nonBlockingWriter{W: ch}
|
||||||
}
|
}
|
||||||
return log.New(io.MultiWriter(os.Stderr, w), "", log.LstdFlags|log.Lshortfile)
|
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 && \
|
RUN apt-get update && \
|
||||||
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
|
||||||
dnsmasq ndisc6 nftables dnsutils strace && \
|
dnsmasq ndisc6 nftables dnsutils strace wireguard iproute2 && \
|
||||||
rm -rf /var/lib/apt/lists/*
|
rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
WORKDIR /usr/src
|
WORKDIR /usr/src
|
||||||
|
6
website/archetypes/default.md
Normal file
6
website/archetypes/default.md
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
---
|
||||||
|
title: "{{ replace .Name "-" " " | title }}"
|
||||||
|
date: {{ .Date }}
|
||||||
|
draft: true
|
||||||
|
---
|
||||||
|
|
19
website/config.toml
Normal file
19
website/config.toml
Normal file
@ -0,0 +1,19 @@
|
|||||||
|
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
|
37
website/content/_index.md
Normal file
37
website/content/_index.md
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
---
|
||||||
|
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.
|
64
website/content/architecture.md
Normal file
64
website/content/architecture.md
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
---
|
||||||
|
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">
|
181
website/content/installation.md
Normal file
181
website/content/installation.md
Normal file
@ -0,0 +1,181 @@
|
|||||||
|
---
|
||||||
|
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).
|
2
website/layouts/robots.txt
Normal file
2
website/layouts/robots.txt
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
User-Agent: *
|
||||||
|
sitemap: https://router7.org/sitemap.xml
|
6
website/layouts/shortcodes/table.html
Normal file
6
website/layouts/shortcodes/table.html
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
{{ $htmlTable := .Inner | markdownify }}
|
||||||
|
{{ $class := .Get 0 }}
|
||||||
|
{{ $old := "<table>" }}
|
||||||
|
{{ $new := printf "<table class=\"%s\">" $class }}
|
||||||
|
{{ $htmlTable := replace $htmlTable $old $new }}
|
||||||
|
{{ $htmlTable | safeHTML }}
|
@ -0,0 +1,19 @@
|
|||||||
|
.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; }
|
@ -0,0 +1 @@
|
|||||||
|
{"Target":"sass/sidebar.css","MediaType":"text/css","Data":{}}
|
1
website/static/CNAME
Normal file
1
website/static/CNAME
Normal file
@ -0,0 +1 @@
|
|||||||
|
router7.org
|
7
website/static/bootstrap-4.4.1.min.css
vendored
Normal file
7
website/static/bootstrap-4.4.1.min.css
vendored
Normal file
File diff suppressed because one or more lines are too long
7
website/static/bootstrap-4.4.1.min.js
vendored
Normal file
7
website/static/bootstrap-4.4.1.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
2
website/static/jquery-3.4.1.slim.min.js
vendored
Normal file
2
website/static/jquery-3.4.1.slim.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
5
website/static/popper-1.16.0.min.js
vendored
Normal file
5
website/static/popper-1.16.0.min.js
vendored
Normal file
File diff suppressed because one or more lines are too long
20
website/themes/router7/LICENSE
Normal file
20
website/themes/router7/LICENSE
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
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.
|
2
website/themes/router7/archetypes/default.md
Normal file
2
website/themes/router7/archetypes/default.md
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
+++
|
||||||
|
+++
|
23
website/themes/router7/assets/sass/sidebar.scss
Normal file
23
website/themes/router7/assets/sass/sidebar.scss
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
.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;
|
||||||
|
}
|
0
website/themes/router7/layouts/404.html
Normal file
0
website/themes/router7/layouts/404.html
Normal file
17
website/themes/router7/layouts/_default/baseof.html
Normal file
17
website/themes/router7/layouts/_default/baseof.html
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
<!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>
|
31
website/themes/router7/layouts/_default/list.html
Normal file
31
website/themes/router7/layouts/_default/list.html
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
{{ 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 }}
|
||||||
|
|
20
website/themes/router7/layouts/_default/single.html
Normal file
20
website/themes/router7/layouts/_default/single.html
Normal file
@ -0,0 +1,20 @@
|
|||||||
|
{{ 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 }}
|
17
website/themes/router7/layouts/index.html
Normal file
17
website/themes/router7/layouts/index.html
Normal file
@ -0,0 +1,17 @@
|
|||||||
|
{{ 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 }}
|
7
website/themes/router7/layouts/partials/footer.html
Normal file
7
website/themes/router7/layouts/partials/footer.html
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
<!-- 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>
|
14
website/themes/router7/layouts/partials/head.html
Normal file
14
website/themes/router7/layouts/partials/head.html
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<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>
|
0
website/themes/router7/layouts/partials/header.html
Normal file
0
website/themes/router7/layouts/partials/header.html
Normal file
15
website/themes/router7/layouts/partials/nav.html
Normal file
15
website/themes/router7/layouts/partials/nav.html
Normal file
@ -0,0 +1,15 @@
|
|||||||
|
<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>
|
21
website/themes/router7/theme.toml
Normal file
21
website/themes/router7/theme.toml
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
# 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