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" "strings" "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"} } // String form used in the layout file and on the page. func prefixString(p netip.Prefix) string { return strings.TrimSpace(p.Masked().String()) }