]> go.fuhry.dev Git - runtime.git/commitdiff
machines bugfixes
authorDan Fuhry <dan@fuhry.com>
Sat, 22 Mar 2025 01:26:52 +0000 (21:26 -0400)
committerDan Fuhry <dan@fuhry.com>
Sat, 22 Mar 2025 01:27:05 +0000 (21:27 -0400)
- handle interface:deleted and host:deleted events
- coredns plugin: only call tryInit if we fail to load the saved state from disk
- coredns plugin: make all hostname lookups case insensitive
- coredns plugin: fix <interface>.<host>.<domain> lookups
- coredns plugin: bump init ticker interval to 10 seconds
- coredns plugin: tighten lock window when updating registry from API
- coredns plugin: increase verbosity level of some really noisy log messages
- tweak mqtt startup
- fix `allow/deny unknown-clients` being added to dhcp subnets with only one range
- add captive portal service in openbsd, fix maclist template

13 files changed:
machines/agent.go
machines/coredns_plugin/registry.go
machines/coredns_plugin/registry_store.go
machines/coredns_plugin/setup.go
machines/event_watcher.go
machines/machines_agent/main.go
machines/render.go
machines/services.go
machines/services_openbsd.go
machines/templates/dhcpd4.conf.j2
machines/templates/dhcpd6.conf.j2
machines/templates/maclist.conf.j2
machines/types.go

index dc5a8cfc72d99d7c7dedc5724d39e1fc0688735d..d66c25a2746b1107a2ef8fc1639eb164a1b3b67f 100644 (file)
@@ -191,10 +191,24 @@ var actionHooks = []*actionHook{
                ),
        },
        {
-               thing:  "iface",
+               thing:  "interface",
                action: "*",
                fileGenerator: multiFile(
-                       staticFiles("dhcpd4.conf", "dhcpd6.conf"),
+                       staticFiles("maclist.conf"),
+               ),
+       },
+       {
+               thing:  "interface",
+               action: "deleted",
+               fileGenerator: multiFile(
+                       staticFiles("dhcpd4.conf", "dhcpd6.conf", "maclist.conf"),
+               ),
+       },
+       {
+               thing:  "host",
+               action: "deleted",
+               fileGenerator: multiFile(
+                       staticFiles("dhcpd4.conf", "dhcpd6.conf", "maclist.conf"),
                ),
        },
 }
index 26a0dfcc8298d61362bbe96f6da41a7fa187d431..854ba098ce14265f0667faa3199219ebcc1698cb 100644 (file)
@@ -107,12 +107,12 @@ func (r *registry) Startup() error {
        } else {
                r.log.Noticef("failed to load saved state from %s, no records will be served "+
                        "until the Machines API is reachable: %v", machinesRegistryStorePath, err)
-       }
 
-       err = r.tryInit()
-       if err != nil {
-               r.log.Warningf("failed to initialize API state, continuing startup; " +
-                       "Machines records will be served once the API is available")
+               err = r.tryInit()
+               if err != nil {
+                       r.log.Warningf("failed to initialize API state, continuing startup; " +
+                               "Machines records will be served once the API is available")
+               }
        }
 
        go r.monitorEvents()
@@ -324,7 +324,7 @@ func (r *registry) LookupHost(qname string) (*Result, error) {
                return nil, fmt.Errorf("plugin not yet initialized")
        }
 
-       fqdn := strings.TrimSuffix(qname, ".")
+       fqdn := strings.ToLower(strings.TrimSuffix(qname, "."))
        r.log.V(2).Debugf("LookupHost(%s)", fqdn)
 
        if strings.HasSuffix(fqdn, ".in-addr.arpa") {
@@ -333,13 +333,13 @@ func (r *registry) LookupHost(qname string) (*Result, error) {
                return r.lookupReverseIPv6(fqdn)
        }
 
-       nameParts := strings.Split(fqdn, ".")
-       basename := strings.ToLower(nameParts[0])
-       domainName := strings.ToLower(strings.Join(nameParts[1:], "."))
+       var basename, domainName string
 
        var domain *machines.Domain
        for _, d := range r.store.Domains {
-               if d.Name == domainName {
+               if strings.HasSuffix(fqdn, "."+d.Name) {
+                       domainName = d.Name
+                       basename = fqdn[:len(fqdn)-len(d.Name)-1]
                        domain = d
                        break
                }
@@ -347,10 +347,22 @@ func (r *registry) LookupHost(qname string) (*Result, error) {
 
        if domain == nil {
                // domain name is not known to us, maybe the query is for a domain at another site
-               r.log.V(1).Debugf("domain is not known to us: %s", domainName)
+               r.log.V(2).Debugf("domain is not known to us: %s", domainName)
                return nil, fmt.Errorf("domain is not known to us: %s", domainName)
        }
 
+       baseParts := strings.Split(basename, ".")
+       switch len(baseParts) {
+       case 1:
+               return r.lookupHostLastSeenIface(basename)
+       case 2:
+               return r.lookupHostWithIface(basename)
+       default:
+               return nil, nil
+       }
+}
+
+func (r *registry) lookupHostLastSeenIface(basename string) (*Result, error) {
        hostID, ok := r.store.HostNames[basename]
        if !ok {
                // host not found
@@ -376,7 +388,50 @@ func (r *registry) LookupHost(qname string) (*Result, error) {
                r.log.V(1).Debugf("host %q: no valid LastDomain.ID", basename)
                return nil, nil
        }
-       domain, ok = r.store.Domains[iface.LastDomain.ID()]
+       domain, ok := r.store.Domains[iface.LastDomain.ID()]
+       if !ok {
+               // last seen domain is not known to us, maybe the host was last seen at a different site
+               r.log.V(1).Debugf("host %q: last seen domain is not known to us: %s", basename, iface.LastDomain.ID())
+               return nil, nil
+       }
+
+       return &Result{
+               domain: domain,
+               host:   host,
+               iface:  iface,
+       }, nil
+}
+
+func (r *registry) lookupHostWithIface(basename string) (*Result, error) {
+       ifaceID, ok := r.store.HostInterfaceNames[basename]
+       if !ok {
+               // host not found
+               r.log.V(1).Debugf("interface.hostname not found: %s", basename)
+               return nil, nil
+       }
+       iface, ok := r.store.Ifaces[ifaceID]
+       if ifaceID == "" || !ok {
+               // host has never been seen on any interface
+               r.log.V(1).Debugf("host %q: host has never been seen on any interface", basename)
+               return nil, nil
+       }
+       if iface.LastDomain.ID() == "" {
+               // no valid record of the last domain this iface was seen on
+               r.log.V(1).Debugf("host %q: no valid LastDomain.ID", basename)
+               return nil, nil
+       }
+       if iface.Host.ID() == "" {
+               r.log.V(1).Debugf("interface %q, basename %q: hostID unset", ifaceID, basename)
+               return nil, nil
+       }
+       hostID := iface.Host.ID()
+       host, ok := r.store.Hosts[hostID]
+       if !ok {
+               // hostname map out of date???
+               r.log.V(1).Debugf("host %q, hostID %s found in hostname table but not in host list", basename, hostID)
+               return nil, fmt.Errorf("hostID %s found in hostname table but not in host list", hostID)
+       }
+       domain, ok := r.store.Domains[iface.LastDomain.ID()]
        if !ok {
                // last seen domain is not known to us, maybe the host was last seen at a different site
                r.log.V(1).Debugf("host %q: last seen domain is not known to us: %s", basename, iface.LastDomain.ID())
@@ -399,7 +454,6 @@ func (r *registry) lookupReverseIPv4(ptrName string) (*Result, error) {
 
        parts = parts[0:4]
        slices.Reverse(parts)
-
        ip := net.ParseIP(strings.Join(parts, "."))
        if ip == nil {
                r.log.Errorf("failed to parse IP in ptr query: %s", strings.Join(parts, "."))
@@ -650,7 +704,9 @@ func (r *registry) processHostSeen(hostID, ifaceID, newIP string) {
                r.store.patchIface(iface)
        }
 
+       host := r.store.Hosts[hostID]
        iface := r.store.Ifaces[ifaceID]
+       host.LastSeenIface.Set(iface)
 
        now := time.Now().Unix()
        iface.LastSeen = machines.Timestamp(now)
@@ -662,31 +718,36 @@ func (r *registry) processHostSeen(hostID, ifaceID, newIP string) {
        }
 
        r.store.patchIface(iface)
+       r.store.patchHost(host)
 }
 
 func (r *registry) tryInit() error {
+       r.log.Noticef("trying to init event listener")
        client, err := machines.NewDefaultMachinesClient()
        if err != nil {
+               r.log.Warningf("init machines client failed: %v", err)
                return err
        }
        client.SetupStats(r.mb)
 
        eventsCh, err := client.NewEventListener(r.ctx)
        if err != nil {
+               r.log.Warningf("init event listener failed: %v", err)
                return err
        }
 
-       if !r.store.initialized() {
+       r.client = client
+       r.eventsCh = eventsCh
+
+       if !r.store.initialized() || !r.store.fresh() {
                err = r.store.fetch(client)
                if err != nil {
+                       r.log.Warningf("fresh data fetch failed: %v", err)
                        return err
                }
                r.didLiveFetch = true
        }
 
-       r.client = client
-       r.eventsCh = eventsCh
-
        return nil
 }
 
@@ -694,9 +755,15 @@ func (r *Result) Answer(ques dns.Question) (int, []dns.RR) {
        answers := make([]dns.RR, 0)
        qname := strings.ToLower(strings.TrimSuffix(ques.Name, "."))
 
-       nameParts := strings.Split(qname, ".")
-       basename := strings.ToLower(nameParts[0])
-       domainName := strings.ToLower(strings.Join(nameParts[1:], "."))
+       var basename, domainName string
+       if strings.HasSuffix(qname, "."+r.domain.Name) {
+               basename = strings.TrimSuffix(qname, "."+r.domain.Name)
+               domainName = qname[len(basename)+1:]
+       } else {
+               nameParts := strings.Split(qname, ".")
+               basename = nameParts[0]
+               domainName = strings.Join(nameParts[1:], ".")
+       }
 
        if strings.HasSuffix(qname, ".in-addr.arpa") || strings.HasSuffix(qname, ".ip6.arpa") {
                if (r.iface.LastIPv4.Defined() || r.iface.LastIPv6.Defined()) && (ques.Qtype == dns.TypePTR || ques.Qtype == dns.TypeANY) {
@@ -722,7 +789,7 @@ func (r *Result) Answer(ques dns.Question) (int, []dns.RR) {
                        Target: fmt.Sprintf("%s.%s.", basename, r.domain.Name),
                }
                answers = append(answers, msg)
-       } else if strings.HasPrefix(qname, r.host.Name+".") {
+       } else if strings.HasPrefix(qname, r.host.Name+".") || strings.HasPrefix(qname, fmt.Sprintf("%s.%s.", r.iface.NameScrubbed, r.host.Name)) {
                if r.iface.LastIPv4.Defined() && (ques.Qtype == dns.TypeA || ques.Qtype == dns.TypeANY) {
                        msg := &dns.A{
                                Hdr: dns.RR_Header{
@@ -781,7 +848,7 @@ func (r *registry) monitorEvents() {
        defer (func() { r.stopCh <- struct{}{} })()
 
        if r.client == nil {
-               initTicker := time.NewTicker(1 * time.Second)
+               initTicker := time.NewTicker(10 * time.Second)
 
        initLoop:
                for {
@@ -821,20 +888,24 @@ func (r *registry) monitorEvents() {
                        r.stats.eventsReceived.WithLabelValues(mbclient.KV{"thing": event.ItemType, "action": event.Event}).Add(1)
                        if event.ItemType == "host" && event.Event == "seen" {
                                if via, ok := event.Tags["via"]; !ok || via != "dhcp" {
+                                       r.log.V(1).Debugf("skip seen event: not via dhcp")
                                        continue
                                }
                                ip, ok := event.Tags["ip"]
                                if !ok || ip == "" {
+                                       r.log.V(1).Debugf("skip seen event: ip tag not present")
                                        continue
                                }
 
                                hostID, ok := event.Tags["host"]
                                if !ok || hostID == "" {
+                                       r.log.V(1).Debugf("skip seen event: host tag not present")
                                        continue
                                }
 
                                ifaceID, ok := event.Tags["iface"]
                                if !ok || ifaceID == "" {
+                                       r.log.V(1).Debugf("skip seen event: iface tag not present")
                                        continue
                                }
 
index 9ba83e8d499e91ba2955c7bd2c290fdb001d4539..1fc9de298e243edc7c1163068061bc8dbd3dff15 100644 (file)
@@ -4,9 +4,11 @@ import (
        "encoding/json"
        "flag"
        "fmt"
+       "net"
        "os"
        "strings"
        "sync"
+       "time"
 
        "go.fuhry.dev/runtime/constants"
        "go.fuhry.dev/runtime/machines"
@@ -17,6 +19,7 @@ type registryStore struct {
        mu  sync.Mutex `json:"-"`
        log *log.Logger
 
+       lastRefresh        time.Time
        Sites              map[string]*machines.Site                      `json:"Sites"`
        Domains            map[string]*machines.Domain                    `json:"Domains"`
        Hosts              map[string]*machines.Host                      `json:"Hosts"`
@@ -47,6 +50,10 @@ func (rs *registryStore) initialized() bool {
        return rs.Sites != nil && rs.Domains != nil && rs.Hosts != nil && rs.Ifaces != nil && rs.Records != nil
 }
 
+func (rs *registryStore) fresh() bool {
+       return rs.lastRefresh.Add(refreshInterval).After(time.Now())
+}
+
 func (rs *registryStore) saveState() error {
        if !rs.initialized() {
                return nil
@@ -105,9 +112,6 @@ func (rs *registryStore) loadState() error {
 }
 
 func (rs *registryStore) fetch(client machines.MachinesClient) error {
-       rs.mu.Lock()
-       defer rs.mu.Unlock()
-
        sites := []*machines.Site{}
        err := client.APICall("sites", nil, &sites)
        if err != nil {
@@ -149,6 +153,9 @@ func (rs *registryStore) fetch(client machines.MachinesClient) error {
                return err
        }
 
+       rs.mu.Lock()
+       defer rs.mu.Unlock()
+
        // update state only after all information is collected
        rs.Sites = make(map[string]*machines.Site)
        for _, s := range sites {
@@ -171,7 +178,7 @@ func (rs *registryStore) fetch(client machines.MachinesClient) error {
        for _, h := range hosts {
                h.Name = strings.ToLower(h.Name)
                rs.Hosts[h.ID()] = h
-               rs.HostNames[h.Name] = h.ID()
+               rs.HostNames[strings.ToLower(h.Name)] = h.ID()
        }
 
        rs.Ifaces = make(map[string]*machines.Iface)
@@ -205,6 +212,7 @@ func (rs *registryStore) fetch(client machines.MachinesClient) error {
        }
 
        rs.processRecords(records)
+       rs.lastRefresh = time.Now()
 
        return nil
 }
@@ -259,3 +267,28 @@ func (rs *registryStore) patchIface(iface *machines.Iface) {
 
        rs.Ifaces[iface.ID()] = iface
 }
+
+func (rs *registryStore) DomainForFqdn(qname string) *machines.Domain {
+       for _, domain := range rs.Domains {
+               if strings.HasSuffix(qname, "."+domain.Name) {
+                       return domain
+               }
+       }
+
+       return nil
+}
+
+func (rs *registryStore) DomainForAddress(addr net.Addr) *machines.Domain {
+       udpAddr, ok := addr.(*net.UDPAddr)
+       if !ok {
+               return nil
+       }
+
+       for _, domain := range rs.Domains {
+               if domain.ContainsIP(udpAddr.IP) {
+                       return domain
+               }
+       }
+
+       return nil
+}
index 54b77750ad05654d0d40695abc6a4f4cb13d72cb..ab828a0921e0a36a9df11488ee80eec575fac6e1 100644 (file)
@@ -57,19 +57,21 @@ func (m *Machines) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Ms
                        Authoritative: true,
                        Response:      true,
                },
-               Answer: make([]dns.RR, 0),
+               Answer: nil,
        }
 
        for _, ques := range r.Question {
                if rc, rrs := m.r.LookupRecord(ques); rc != dns.RcodeNameError {
                        domain := m.r.(*registry).domainFromQuestion(ques)
-                       m.r.(*registry).stats.recordLookups.WithLabelValues(mbclient.KV{
-                               "domain": domain.Name,
-                               "rcode":  rcodeToStr(rc),
-                       }).Add(1)
+                       if domain != nil {
+                               m.r.(*registry).stats.recordLookups.WithLabelValues(mbclient.KV{
+                                       "domain": domain.Name,
+                                       "rcode":  rcodeToStr(rc),
+                               }).Add(1)
+                       }
                        handled = true
                        answer.Question = append(answer.Question, ques)
-                       m.log.V(2).Debugf("  -> rcode %d, rrs %+v", rcode, rrs)
+                       m.log.V(3).Debugf("  -> rcode %d, rrs %+v", rcode, rrs)
                        if rc != dns.RcodeSuccess {
                                m.log.Errorf("error in LookupRecord(%s): rcode=%d", ques.Name, rc)
                                rcode = rc
@@ -82,7 +84,7 @@ func (m *Machines) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Ms
                        handled = true
                        answer.Question = append(answer.Question, ques)
                        if result == nil {
-                               m.log.V(2).Debugf("  -> not found")
+                               m.log.V(3).Debugf("  -> not found")
                                rcode = dns.RcodeNameError
 
                                m.r.(*registry).stats.hostLookups.WithLabelValues(mbclient.KV{
@@ -90,7 +92,7 @@ func (m *Machines) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Ms
                                        "rcode":  rcodeToStr(rcode),
                                }).Add(1)
                        } else {
-                               m.log.V(2).Debugf("  -> domain %s, host %s, iface %s", result.domain.ID(), result.host.ID(), result.iface.ID())
+                               m.log.V(3).Debugf("  -> domain %s, host %s, iface %s", result.domain.ID(), result.host.ID(), result.iface.ID())
 
                                r, rrs := result.Answer(ques)
                                m.r.(*registry).stats.hostLookups.WithLabelValues(mbclient.KV{
@@ -111,7 +113,7 @@ func (m *Machines) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Ms
        }
 
        if handled {
-               m.log.V(3).Debugf("  -> WRITE %+v", answer)
+               m.log.V(4).Debugf("  -> WRITE %+v", answer)
                answer.MsgHdr.Rcode = rcode
                err := w.WriteMsg(answer)
                if err != nil {
@@ -120,6 +122,6 @@ func (m *Machines) ServeDNS(ctx context.Context, w dns.ResponseWriter, r *dns.Ms
                }
                return rcode, nil
        }
-       m.log.V(3).Debugf("NEXT!")
+       m.log.V(4).Debugf("NEXT!")
        return plugin.NextOrFailure(m.Name(), m.Next, ctx, w, r)
 }
index 4efa6320b61ce2eb566fa73a695157f66dae1b2f..1b87c90dfcb4851806c1b5cbb41391d5ba041ee7 100644 (file)
@@ -3,7 +3,6 @@ package machines
 import (
        "context"
        "encoding/json"
-       "errors"
        "fmt"
        "net/url"
        "time"
@@ -39,6 +38,8 @@ func (mc *machinesClient) NewEventListener(ctx context.Context) (chan MachinesMq
                return nil, fmt.Errorf("events url %q is invalid: %v", mc.eventsUrl, err)
        }
 
+       msgChan := make(chan MachinesMqttEvent)
+
        credentialsProvider := func() (string, string) {
                accessToken, err := oauthClient.getAccessToken()
                if err != nil {
@@ -50,34 +51,7 @@ func (mc *machinesClient) NewEventListener(ctx context.Context) (chan MachinesMq
                return accessToken, "x"
        }
 
-       mqttOpts.Servers = append(mqttOpts.Servers, eventsUrl)
-       mqttOpts.SetCredentialsProvider(credentialsProvider)
-       mqttOpts.SetPingTimeout(10 * time.Second)
-       mqttOpts.SetConnectTimeout(10 * time.Second)
-       mqttOpts.SetCleanSession(true)
-       mqttOpts.SetAutoReconnect(true)
-       mqttOpts.SetConnectRetry(false)
-       mqttOpts.SetMaxReconnectInterval(30 * time.Second)
-
-       client := mqtt.NewClient(mqttOpts)
-       t := client.Connect()
-
-       select {
-       case <-t.Done():
-               if err = t.Error(); err != nil {
-                       return nil, err
-               }
-       case <-ctx.Done():
-               return nil, context.Canceled
-       }
-
-       if !client.IsConnected() {
-               return nil, errors.New("somehow we are still not connected??")
-       }
-
-       msgChan := make(chan MachinesMqttEvent)
-
-       handler := func(client mqtt.Client, msg mqtt.Message) {
+       onMessage := func(client mqtt.Client, msg mqtt.Message) {
                msg.Ack()
 
                mc.logger.V(2).Debugf("got raw mqtt msg: %s", msg.Payload())
@@ -102,12 +76,37 @@ func (mc *machinesClient) NewEventListener(ctx context.Context) (chan MachinesMq
                }
        }
 
-       t = client.Subscribe(constants.MachinesMqttTopic, byte(0), handler)
+       onConnect := func(client mqtt.Client) {
+               mqttLogger.Noticef("(Re)connected to server")
+               t := client.Subscribe(constants.MachinesMqttTopic, byte(0), onMessage)
+               select {
+               case <-t.Done():
+                       mqttLogger.Noticef("(Re)subscribed to topic")
+                       break
+               case <-ctx.Done():
+                       close(msgChan)
+               }
+       }
+
+       mqttOpts.Servers = append(mqttOpts.Servers, eventsUrl)
+       mqttOpts.SetCredentialsProvider(credentialsProvider)
+       mqttOpts.SetPingTimeout(10 * time.Second)
+       mqttOpts.SetConnectTimeout(10 * time.Second)
+       mqttOpts.SetCleanSession(true)
+       mqttOpts.SetAutoReconnect(true)
+       mqttOpts.SetConnectRetry(true)
+       mqttOpts.SetMaxReconnectInterval(30 * time.Second)
+       mqttOpts.SetOnConnectHandler(onConnect)
+
+       client := mqtt.NewClient(mqttOpts)
+       t := client.Connect()
+
        select {
        case <-t.Done():
-               break
+               if err = t.Error(); err != nil {
+                       return nil, err
+               }
        case <-ctx.Done():
-               close(msgChan)
                return nil, ctx.Err()
        }
 
index 04052ccdbe52470ae5c6606619547595951f1ab6..af9546deadc92eb08d721230d38c746f7dabbe37 100644 (file)
@@ -216,7 +216,7 @@ func main() {
        }
 
        logger.Noticef("starting main event loop")
-       defer logger.Critical("stopping xx0r-machines-agent")
+       defer logger.Critical("stopping machines-agent")
 
 mainLoop:
        for {
index bfc8b0fb388959061a3d006ec36416ccf95f71fd..b79408b8cdfb11a5c82032776c5052c84f76b6f8 100644 (file)
@@ -42,6 +42,7 @@ var templateVarMap = varMap{
        "rad.conf":               {"domains", "site", "routeraddresses"},
        "radvd.conf":             {"domains", "site", "routeraddresses"},
        "pf-captive-portal.conf": {"domains", "apiServer"},
+       "maclist.conf":           {"domains", "interfaces"},
 }
 
 var baseVars = []string{
@@ -78,6 +79,10 @@ var outputFileMap = map[string]*artifact{
                filePath:    "/etc/radvd.conf",
                serviceName: "radvd",
        },
+       "maclist.conf": {
+               filePath:    "/etc/pf/maclist.conf",
+               serviceName: "captive",
+       },
 }
 
 type calculator struct {
@@ -359,7 +364,7 @@ func (c *calculator) realGet(varName string, args ...string) (any, error) {
                return sitesMap, nil
        case "interfaces":
                ifaces := make([]*Iface, 0)
-               err := c.client.APICall("interfaces", nil, &ifaces)
+               err := c.client.APICall("interfaces?expand[]=hosts", nil, &ifaces)
                if err != nil {
                        return nil, fmt.Errorf("while fetching interfaces: %+v", err)
                }
index 4e71146b6095dc0a0e9d2d6fcf2d712e6f479976..ed20751a68f02edc51e8a513be84ebb1006fa8f6 100644 (file)
@@ -1,5 +1,12 @@
 package machines
 
+import (
+       "os/exec"
+       "path"
+
+       "go.fuhry.dev/runtime/utils/log"
+)
+
 type ServiceStatus struct {
        Running bool
        Pid     int
@@ -22,6 +29,52 @@ func (s *noopService) Status() (*ServiceStatus, error) {
        return &ServiceStatus{Running: true, Pid: 0}, nil
 }
 
+type oneshotService struct {
+       command     []string
+       stopCommand []string
+}
+
+func (s *oneshotService) EnsureStarted() error {
+       return s.do(s.command)
+}
+
+func (s *oneshotService) EnsureStopped() error {
+       if s.stopCommand != nil {
+               return s.do(s.stopCommand)
+       }
+
+       return nil
+}
+
+func (s *oneshotService) ReloadOrRestart(startIfStopped bool) error {
+       return s.do(s.command)
+}
+
+func (s *oneshotService) Status() (*ServiceStatus, error) {
+       return &ServiceStatus{
+               Running: true,
+               Pid:     0,
+       }, nil
+}
+
+func (s *oneshotService) do(cmdline []string) error {
+       executable := cmdline[0]
+       if !path.IsAbs(executable) {
+               exe, err := exec.LookPath(executable)
+               if err != nil {
+                       return err
+               }
+               executable = exe
+       }
+
+       logger.Noticef("running command: %s %+v", executable, cmdline[1:])
+       cmd := exec.Command(executable, cmdline[1:]...)
+       cmd.Stdout = logger.AppendPrefix("[stdout]").NewWriter(log.INFO)
+       cmd.Stderr = logger.AppendPrefix("[stderr]").NewWriter(log.ERROR)
+       err := cmd.Run()
+       return err
+}
+
 var services = make(map[string]Service)
 
 func registerService(name string, svc Service) {
index 3ceb26d586d083c4d3ddc3ce10dfb68d241674e6..b34041642958333f8ff259453c9bcce002800e6f 100644 (file)
@@ -178,4 +178,7 @@ func init() {
                pidFile:    "/var/run/dhcpd6.pid",
                daemon:     true,
        })
+       registerService("captive", &oneshotService{
+               command: []string{"bash", "-c", "for f in /etc/hostname.bridge*; do /sbin/ifconfig ${f#/etc/hostname.} flushrule vlan${f#/etc/hostname.bridge} rulefile /etc/pf/maclist.conf; done"},
+       })
 }
index 67dbc09496157a56982400c0b18a399464edc735..be513cda55e94ef0d36a0870cce5fd8a229a7ec4 100644 (file)
@@ -63,11 +63,13 @@ subnet {{ domain.IPv4Address }} netmask {{ domain.IPv4PrefixLength.Mask() }} {
        # {{ range.Name }}
        pool {
                range {{ domain.Ranges[id].IPv4Start }} {{ domain.Ranges[id].IPv4End }};
-                       {% if domain.Ranges[id].ID() == domain.DefaultRange.ID() %}
-                       allow unknown-clients;
-                       {%- else -%}
-                       deny unknown-clients;
-                       {%- endif %}
+                       {%- if domain.Ranges.length > 1 -%}
+                               {% if domain.Ranges[id].ID() == domain.DefaultRange.ID() %}
+                               allow unknown-clients;
+                               {%- else -%}
+                               deny unknown-clients;
+                               {%- endif %}
+                       {%- endif -%}
 
                        {% for res in domain.Ranges[id].Reservations %}
                        {%- if res.AddressFamily == 'inet' -%}
index 683088eeafaba77feac86f4a03f54fe6c42d600a..9904ea3080efb24a95c7241013c78a4e6fce5628 100644 (file)
@@ -49,11 +49,13 @@ on commit {
                                        # {{ range.Name }}
                                        pool6 {
                                                range6 {{ range.IPv6Start }} {{ range.IPv6End }};
-                                               {% if range.ID() == domain.DefaultRange.ID() %}
-                                               allow unknown-clients;
-                                               {%- else -%}
-                                               deny unknown-clients;
-                                               {%- endif %}
+                                               {%- if domain.Ranges.length > 1 -%}
+                                                       {% if range.ID() == domain.DefaultRange.ID() %}
+                                                       allow unknown-clients;
+                                                       {%- else -%}
+                                                       deny unknown-clients;
+                                                       {%- endif %}
+                                               {%- endif -%}
 
                                                {%- for res in range.Reservations -%}
                                                        {% if res.AddressFamily == 'inet6' %}
index b283f679eb886c374970fe4b33df0c81bf8a1b11..d28f5f74f9c75319b85e9acc19573ffda0bd5c90 100644 (file)
@@ -1,8 +1,12 @@
 # This file is automatically generated by machines-agent and will be overwritten
 # any time a change is made to registered hosts/interfaces.
 
-{% for iface in interfaces -%}
-{% if iface.host != None and 'disabled' not in iface.host.flags and 'quarantine' not in iface.host.owner.flags -%}
-pass in on vlan{{ domain.vlan_id }} src {{ iface.hardware_address }} tag PERMIT
+{% for domain in domains %}
+{%- if 'captive_portal' in domain.Features %}
+{%- for iface in interfaces -%}
+{% if iface.Host.Defined() -%}
+pass in on vlan{{ domain.VlanID }} src {{ iface.HardwareAddress }} tag PERMIT
 {% endif -%}
-{% endfor -%}
+{%- endfor -%}
+{%- endif -%}
+{%- endfor -%}
\ No newline at end of file
index b9e19238f2b68f7793e7b0001ab90c546d8c5929..909592641450cda0250b424436109bf13244adf4 100644 (file)
@@ -372,6 +372,30 @@ type EndorsementKey struct {
        } `json:"endorsement_key"`
 }
 
+func (d *Domain) ContainsIP(ip net.IP) bool {
+       if ip4 := ip.To4(); ip4 != nil {
+
+       } else if ip6 := ip.To16(); ip6 != nil {
+
+       }
+
+       return false
+}
+
+func (d *Domain) IPv4Network() net.IPNet {
+       return net.IPNet{
+               IP:   d.IPv4Address.AsIP(),
+               Mask: d.IPv4PrefixLength.IPMask(),
+       }
+}
+
+func (d *Domain) IPv6Network() net.IPNet {
+       return net.IPNet{
+               IP:   d.IPv6Address.AsIP(),
+               Mask: d.IPv6PrefixLength.IPMask(),
+       }
+}
+
 func (t Timestamp) AsTime() time.Time {
        return time.Unix(int64(t), 0)
 }