Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 8 additions & 4 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,18 +27,20 @@ jobs:
env:
CGO_ENABLED: "1"
GOOS: darwin
RELEASE_TAG: ${{ github.event.release.tag_name }}
GOARCH: arm64
run: |
go build -trimpath -ldflags='-s -w' -o ../mac_enclosure_color.arm64.ext .
go build -trimpath -ldflags="-s -w -X main.version=${RELEASE_TAG#v}" -o ../mac_enclosure_color.arm64.ext .

- name: Build (amd64)
working-directory: mac_enclosure_color
env:
CGO_ENABLED: "1"
GOOS: darwin
RELEASE_TAG: ${{ github.event.release.tag_name }}
GOARCH: amd64
run: |
go build -trimpath -ldflags='-s -w' -o ../mac_enclosure_color.amd64.ext .
go build -trimpath -ldflags="-s -w -X main.version=${RELEASE_TAG#v}" -o ../mac_enclosure_color.amd64.ext .

- name: Create universal binary
run: |
Expand Down Expand Up @@ -76,18 +78,20 @@ jobs:
env:
CGO_ENABLED: "1"
GOOS: darwin
RELEASE_TAG: ${{ github.event.release.tag_name }}
GOARCH: arm64
run: |
go build -trimpath -ldflags='-s -w' -o ../dot1x.arm64.ext .
go build -trimpath -ldflags="-s -w -X main.version=${RELEASE_TAG#v}" -o ../dot1x.arm64.ext .

- name: Build (amd64)
working-directory: dot1x
env:
CGO_ENABLED: "1"
GOOS: darwin
RELEASE_TAG: ${{ github.event.release.tag_name }}
GOARCH: amd64
run: |
go build -trimpath -ldflags='-s -w' -o ../dot1x.amd64.ext .
go build -trimpath -ldflags="-s -w -X main.version=${RELEASE_TAG#v}" -o ../dot1x.amd64.ext .

- name: Create universal binary
run: |
Expand Down
26 changes: 26 additions & 0 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
name: test

on:
pull_request:
push:
branches: [main]

permissions:
contents: read

jobs:
test:
runs-on: macos-latest
steps:
- uses: actions/checkout@v4
Comment thread
coderabbitai[bot] marked this conversation as resolved.
with:
persist-credentials: false
- uses: actions/setup-go@v5
with:
go-version-file: extserver/go.mod
cache-dependency-path: "*/go.sum"
- name: vet and test
run: |
for m in extserver dot1x mac_enclosure_color; do
(cd "$m" && go vet ./... && go test -race ./...)
done
2 changes: 1 addition & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ build:
done

test:
@for ext in $(EXTENSIONS); do \
@for ext in extserver $(EXTENSIONS); do \
cd $$ext && go test ./... && cd ..; \
done

Expand Down
6 changes: 5 additions & 1 deletion dot1x/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -2,23 +2,27 @@ module github.com/CampusTech/osquery-extensions/dot1x

go 1.26.2

replace github.com/macadmins/osquery-extension => github.com/robbiet480/osquery-extension v1.4.2-0.20260610232042-f9087b9cdba5
replace github.com/macadmins/osquery-extension => github.com/robbiet480/osquery-extension v1.4.2-0.20261003192026-0f992a0b9fe5

require (
github.com/macadmins/osquery-extension v0.0.0-00010101000000-000000000000
github.com/osquery/osquery-go v0.0.0-20260630173615-eb39ad3443df
)

require (
github.com/CampusTech/osquery-extensions/extserver v0.0.0
github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/apache/thrift v0.24.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect
github.com/micromdm/plist v0.2.3-0.20260123201933-667adaf87d87 // indirect
github.com/pkg/errors v0.9.1 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/otel v1.41.0 // indirect
go.opentelemetry.io/otel/metric v1.41.0 // indirect
go.opentelemetry.io/otel/trace v1.41.0 // indirect
golang.org/x/sys v0.36.0 // indirect
)

replace github.com/CampusTech/osquery-extensions/extserver => ../extserver
6 changes: 4 additions & 2 deletions dot1x/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -13,14 +13,16 @@ github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/micromdm/plist v0.2.3-0.20260123201933-667adaf87d87 h1:U9A+0ZED3cPxb5ufiTzyn2kyo6UFoR5bMggCR0Q/DOg=
github.com/micromdm/plist v0.2.3-0.20260123201933-667adaf87d87/go.mod h1:flkfm0od6GzyXBqI28h5sgEyi3iPO28W2t1Zm9LpwWs=
github.com/osquery/osquery-go v0.0.0-20260630173615-eb39ad3443df h1:GEa8SFf/OXRXSHU5EtjUxs4yPz8caf9UlHN/F89/OJA=
github.com/osquery/osquery-go v0.0.0-20260630173615-eb39ad3443df/go.mod h1:/Nxgn14on72pNnpjn9Fbw9WQc8JKY1sVv5K+SRZknEk=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/robbiet480/osquery-extension v1.4.2-0.20260610232042-f9087b9cdba5 h1:Lvgw8XV0rpx4TH+4cina26OCgxVg9ykAk5eNU10k11o=
github.com/robbiet480/osquery-extension v1.4.2-0.20260610232042-f9087b9cdba5/go.mod h1:WFTX1rypPMYrDKC/0de0sy/g9rDEdlj4JXkYNgd+XDM=
github.com/robbiet480/osquery-extension v1.4.2-0.20261003192026-0f992a0b9fe5 h1:yGMO+O5Fortz2PLy6rVPWBsaq30iq012WTDksmIupXI=
github.com/robbiet480/osquery-extension v1.4.2-0.20261003192026-0f992a0b9fe5/go.mod h1:bfYLdHLnY32gcICdfzV/InPa9ZZLLnZ9P5R7hPGkw8s=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
Expand Down
41 changes: 9 additions & 32 deletions dot1x/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -13,48 +13,25 @@
//
// Build:
//
// GOOS=darwin go build -o dot1x.ext
// GOOS=darwin go build -ldflags "-X main.version=0.4.1" -o dot1x.ext
//
// Run standalone (for testing):
//
// osqueryi --extension ./dot1x.ext
package main

import (
"flag"
"log"
"time"

"github.com/CampusTech/osquery-extensions/extserver"
"github.com/macadmins/osquery-extension/tables/dot1x"
osquery "github.com/osquery/osquery-go"
"github.com/osquery/osquery-go/plugin/table"
)

func main() {
socket := flag.String("socket", "", "Path to the osquery extension socket")
timeout := flag.Int("timeout", 3, "Seconds to wait for a successful connection")
interval := flag.Int("interval", 3, "Seconds between connection checks")
verbose := flag.Bool("verbose", false, "Enable verbose extension logging")
flag.Parse()
_ = *verbose

if *socket == "" {
log.Fatalln("--socket is required")
}
// version is reported in osquery_extensions.version; the Fleet install
// policy gates on it. Release builds set it with
// -ldflags "-X main.version=<tag without v>".
var version = "dev"

server, err := osquery.NewExtensionManagerServer(
"dot1x",
*socket,
osquery.ServerTimeout(time.Duration(*timeout)*time.Second),
osquery.ServerPingInterval(time.Duration(*interval)*time.Second),
)
if err != nil {
log.Fatalf("error creating extension manager: %s", err)
}

server.RegisterPlugin(table.NewPlugin("dot1x", dot1x.Dot1XStatusColumns(), dot1x.Dot1XStatusGenerate))

if err := server.Run(); err != nil {
log.Fatalln(err)
}
func main() {
extserver.Main("dot1x", version,
table.NewPlugin("dot1x", dot1x.Dot1XStatusColumns(), dot1x.Dot1XStatusGenerate))
}
186 changes: 186 additions & 0 deletions extserver/extserver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
// Package extserver runs a Campus osquery extension. It replaces osquery-go's
// ExtensionManagerServer.Run, whose failure handling wedged osqueryd's
// extension manager on several Macs (first seen on Fleet host 671,
// 2026-10-05):
//
// - Run exits on the first failed ping. "timeout after 200ms" is not
// osqueryd being gone: it is osquery-go's client lock wait (200ms by
// default), which can fail even uncontended after a scheduling stall.
// - The exited extension leaves its registration for osqueryd to reap, and
// osqueryd 5.23.1 can block reaping indefinitely behind a running query.
// Every respawn is then refused with "Duplicate extension registered" and
// exits, every ~3s, until osqueryd restarts.
//
// Run instead waits up to the connection timeout for the client lock,
// tolerates transient ping failures, retries duplicate registrations
// in-process with backoff, and always deregisters on the way out (including
// on SIGTERM/SIGINT).
package extserver

import (
"context"
"errors"
"flag"
"fmt"
"log"
"os"
"os/signal"
"strings"
"sync"
"syscall"
"time"

osquery "github.com/osquery/osquery-go"
gen "github.com/osquery/osquery-go/gen/osquery"
)

const (
// maxPingFailures consecutive failed pings mean osquery is really gone.
maxPingFailures = 3
// maxRetryBackoff caps the wait between duplicate-registration retries.
maxRetryBackoff = time.Minute
)

// Main parses the standard osquery extension flags (--socket, --timeout,
// --interval, --verbose), registers plugins as extension name/version and
// serves until osquery goes away or the process is signalled.
func Main(name, version string, plugins ...osquery.OsqueryPlugin) {
socket := flag.String("socket", "", "Path to the osquery extension socket")
timeoutSec := flag.Int("timeout", 3, "Seconds to wait for a successful connection")
intervalSec := flag.Int("interval", 3, "Seconds between connection checks")
verbose := flag.Bool("verbose", false, "Enable verbose extension logging")
flag.Parse()
_ = *verbose

if *socket == "" {
log.Fatalln("--socket is required")
}
timeout := time.Duration(*timeoutSec) * time.Second
interval := time.Duration(*intervalSec) * time.Second

// Wait up to timeout for the client's socket lock instead of osquery-go's
// 200ms default.
client, err := osquery.NewClient(*socket, timeout, osquery.DefaultWaitTime(timeout))
if err != nil {
log.Fatalf("error creating extension manager client: %s", err)
}

server, gated, err := newServer(name, version, *socket, client, timeout, plugins...)
if err != nil {
client.Close()
log.Fatalf("error creating extension manager: %s", err)
}

ctx, stop := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
err = run(ctx, server, gated, interval)
stop()
client.Close()
if err != nil {
log.Fatalln(err)
}
}

// gate refuses registrations once shut, so none can begin after run starts
// shutting down. osquery-go only guards this for clients it creates itself.
type gate struct {
osquery.ExtensionManager
mu sync.Mutex
shut bool
}

func (g *gate) RegisterExtension(info *gen.InternalExtensionInfo, registry gen.ExtensionRegistry) (*gen.ExtensionStatus, error) {
g.mu.Lock()
defer g.mu.Unlock()
if g.shut {
return nil, errors.New("shutting down")
}
return g.ExtensionManager.RegisterExtension(info, registry)
}

// close waits out an in-flight registration (so Shutdown deregisters it) and
// refuses any later one.
func (g *gate) close() {
g.mu.Lock()
g.shut = true
g.mu.Unlock()
}

func newServer(name, version, socket string, client osquery.ExtensionManager, timeout time.Duration, plugins ...osquery.OsqueryPlugin) (*osquery.ExtensionManagerServer, *gate, error) {
g := &gate{ExtensionManager: client}
server, err := osquery.NewExtensionManagerServer(
name,
socket,
osquery.WithClient(g),
osquery.ServerTimeout(timeout),
osquery.ExtensionVersion(version),
)
if err != nil {
return nil, nil, err
}
server.RegisterPlugin(plugins...)
return server, g, nil
}

// run registers the extension and serves until ctx is cancelled, osquery asks
// it to shut down, or maxPingFailures consecutive pings fail.
func run(ctx context.Context, server *osquery.ExtensionManagerServer, client *gate, interval time.Duration) error {
// Always deregister on the way out so osqueryd never has to reap a stale
// registration itself.
defer func() {
client.close()
if err := server.Shutdown(context.Background()); err != nil {
log.Println(err)
}
}()
ctx, cancel := context.WithCancel(ctx)
defer cancel() // runs before Shutdown: stops registration retries

served := make(chan error, 1)
go func() { served <- start(ctx, server, interval) }()
Comment thread
coderabbitai[bot] marked this conversation as resolved.

ticker := time.NewTicker(interval)
defer ticker.Stop()
failures := 0
for {
select {
case <-ctx.Done():
return nil
case err := <-served:
return err
case <-ticker.C:
status, err := client.Ping()
if err == nil && status.Code != 0 {
err = fmt.Errorf("status %d: %s", status.Code, status.Message)
}
if err == nil {
failures = 0
continue
}
failures++
if failures >= maxPingFailures {
return fmt.Errorf("osquery unreachable, %d pings failed: %w", failures, err)
}
log.Printf("ping %d/%d failed: %v", failures, maxPingFailures, err)
}
}
}

// start registers and serves. While osqueryd still holds a stale registration
// with our name it refuses us as a duplicate; its extension watcher normally
// reaps the stale entry within a couple of --extensions_interval periods, so
// retry in-process with backoff instead of exiting into a respawn loop.
func start(ctx context.Context, server *osquery.ExtensionManagerServer, backoff time.Duration) error {
for {
err := server.Start()
if err == nil || !strings.Contains(err.Error(), "Duplicate extension registered") {
return err
}
log.Printf("%v; retrying in %s", err, backoff)
select {
case <-ctx.Done():
return nil
case <-time.After(backoff):
}
backoff = min(2*backoff, maxRetryBackoff)
}
}
Loading
Loading