From the race detector, staticcheck, a garbage-in battery over every safe route and a
headless pass through every tab on two machines. Fixed:
- docker inspect/health of a container that isn't there answered 500; now 404, and a
daemon that is down 503 (dockerErr, with a test).
- /api/virt/instances/{name} on a machine without Incus answered 502 "incus not found";
it now says Incus isn't ready, like its siblings.
- Deleting a ZFS snapshot with a bad name answered 500; a bad name is 400, a snapshot
that doesn't exist 404.
- The task list read a task's Running flag without the task's lock while the waiter
goroutine cleared it: a data race, now read under the lock.
- The Virtualization export config and run record were written in place; a crash
mid-write would have left a half file. Atomic now, like the config and state files.
- An unused function in kvm.go, one in incus/subnet.go, an unused field in files/move.go
and a capitalised error string, from staticcheck.
Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
127 lines
3.8 KiB
Go
127 lines
3.8 KiB
Go
package incus
|
||
|
||
// The bridge subnet. Incus's own `auto` picks a random 10.x.y.0/24, and on a LAN that is
|
||
// itself 10.0.0.0/8 (this one) every such pick is on-link and collides. So Ham Lab chooses:
|
||
// the first /24 that nothing on the machine already routes, is not an address it holds, and
|
||
// is outside Docker's configured pools — tried in an order that also stays clear of Docker's
|
||
// *defaults* (172.17–172.31 and 192.168.0.0/16 carved into /20s), which a Docker that has no
|
||
// daemon.json will hand out later without asking anyone.
|
||
|
||
import (
|
||
"context"
|
||
"encoding/json"
|
||
"fmt"
|
||
"net/netip"
|
||
"os"
|
||
"os/exec"
|
||
"time"
|
||
)
|
||
|
||
// candidates in order of preference: 172.16.0.0/16 is the one RFC 1918 block Docker never
|
||
// takes by default; then the top of 192.168/16, which Docker reaches last; then the rest.
|
||
func candidates() []netip.Prefix {
|
||
var out []netip.Prefix
|
||
for i := 100; i <= 254; i++ {
|
||
out = append(out, netip.MustParsePrefix(fmt.Sprintf("172.16.%d.0/24", i)))
|
||
}
|
||
for i := 254; i >= 200; i-- {
|
||
out = append(out, netip.MustParsePrefix(fmt.Sprintf("192.168.%d.0/24", i)))
|
||
}
|
||
for i := 1; i < 100; i++ {
|
||
out = append(out, netip.MustParsePrefix(fmt.Sprintf("172.16.%d.0/24", i)))
|
||
}
|
||
return out
|
||
}
|
||
|
||
func overlaps(a, b netip.Prefix) bool {
|
||
return a.Contains(b.Addr()) || b.Contains(a.Addr())
|
||
}
|
||
|
||
// PickSubnet returns the first candidate /24 clear of every prefix in taken.
|
||
func PickSubnet(taken []netip.Prefix) netip.Prefix {
|
||
for _, c := range candidates() {
|
||
clear := true
|
||
for _, t := range taken {
|
||
if overlaps(c, t) {
|
||
clear = false
|
||
break
|
||
}
|
||
}
|
||
if clear {
|
||
return c
|
||
}
|
||
}
|
||
return netip.MustParsePrefix("172.16.100.0/24") // every candidate taken: not a machine this will run on
|
||
}
|
||
|
||
// Gateway is the bridge's own address in a subnet: its first host, as CIDR, the way
|
||
// Incus's ipv4.address wants it.
|
||
func Gateway(p netip.Prefix) string {
|
||
return fmt.Sprintf("%s/%d", p.Masked().Addr().Next(), p.Bits())
|
||
}
|
||
|
||
// TakenPrefixes reads what the machine already uses: its routes, its addresses, and the
|
||
// pools Docker is configured to carve networks from.
|
||
func TakenPrefixes(ctx context.Context) []netip.Prefix {
|
||
var out []netip.Prefix
|
||
add := func(s string) {
|
||
if p, err := netip.ParsePrefix(s); err == nil && p.Addr().Is4() {
|
||
out = append(out, p.Masked())
|
||
} else if a, err := netip.ParseAddr(s); err == nil && a.Is4() {
|
||
out = append(out, netip.PrefixFrom(a, 32))
|
||
}
|
||
}
|
||
c, cancel := context.WithTimeout(ctx, 5*time.Second)
|
||
defer cancel()
|
||
if b, err := exec.CommandContext(c, "ip", "-j", "-4", "route", "show").Output(); err == nil {
|
||
var routes []struct{ Dst string }
|
||
if json.Unmarshal(b, &routes) == nil {
|
||
for _, r := range routes {
|
||
if r.Dst != "" && r.Dst != "default" {
|
||
add(r.Dst)
|
||
}
|
||
}
|
||
}
|
||
}
|
||
if b, err := exec.CommandContext(c, "ip", "-j", "-4", "addr", "show").Output(); err == nil {
|
||
var links []struct {
|
||
AddrInfo []struct {
|
||
Local string `json:"local"`
|
||
Prefixlen int `json:"prefixlen"`
|
||
} `json:"addr_info"`
|
||
}
|
||
if json.Unmarshal(b, &links) == nil {
|
||
for _, l := range links {
|
||
for _, a := range l.AddrInfo {
|
||
add(fmt.Sprintf("%s/%d", a.Local, a.Prefixlen))
|
||
}
|
||
}
|
||
}
|
||
}
|
||
for _, p := range dockerPools() {
|
||
add(p)
|
||
}
|
||
return out
|
||
}
|
||
|
||
// dockerPools reads default-address-pools from Docker's daemon.json, and falls back to
|
||
// Docker's built-in defaults when the file names none.
|
||
func dockerPools() []string {
|
||
var cfg struct {
|
||
Pools []struct {
|
||
Base string `json:"base"`
|
||
} `json:"default-address-pools"`
|
||
}
|
||
if b, err := os.ReadFile("/etc/docker/daemon.json"); err == nil && json.Unmarshal(b, &cfg) == nil && len(cfg.Pools) > 0 {
|
||
var out []string
|
||
for _, p := range cfg.Pools {
|
||
out = append(out, p.Base)
|
||
}
|
||
return out
|
||
}
|
||
if _, err := exec.LookPath("docker"); err != nil {
|
||
return nil
|
||
}
|
||
return []string{"172.17.0.0/12", "192.168.0.0/16"}
|
||
}
|