dhcp4d: display MAC vendor of each lease’s HardwareAddr
This commit is contained in:
parent
8df6329209
commit
6320b6c3a7
@ -18,6 +18,7 @@ package main
|
||||
import (
|
||||
"encoding/json"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
@ -37,6 +38,7 @@ import (
|
||||
"github.com/rtr7/router7/internal/dhcp4d"
|
||||
"github.com/rtr7/router7/internal/multilisten"
|
||||
"github.com/rtr7/router7/internal/notify"
|
||||
"github.com/rtr7/router7/internal/oui"
|
||||
"github.com/rtr7/router7/internal/teelogger"
|
||||
)
|
||||
|
||||
@ -59,6 +61,8 @@ func updateNonExpired(leases []*dhcp4d.Lease) {
|
||||
nonExpiredLeases.Set(float64(nonExpired))
|
||||
}
|
||||
|
||||
var ouiDB = oui.NewDB("/perm/dhcp4d/oui")
|
||||
|
||||
func loadLeases(h *dhcp4d.Handler, fn string) error {
|
||||
b, err := ioutil.ReadFile(fn)
|
||||
if err != nil {
|
||||
@ -73,6 +77,14 @@ func loadLeases(h *dhcp4d.Handler, fn string) error {
|
||||
}
|
||||
h.SetLeases(leases)
|
||||
updateNonExpired(leases)
|
||||
|
||||
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||
// TODO: html template
|
||||
for _, l := range leases {
|
||||
fmt.Fprintf(w, "• %+v (vendor %v)\n", l, ouiDB.Lookup(l.HardwareAddr[:8]))
|
||||
}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
|
168
internal/oui/oui.go
Normal file
168
internal/oui/oui.go
Normal file
@ -0,0 +1,168 @@
|
||||
package oui
|
||||
|
||||
import (
|
||||
"encoding/csv"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
|
||||
"github.com/google/renameio"
|
||||
)
|
||||
|
||||
// DB is a IEEE MA-L (MAC Address Block Large, formerly known as OUI) database.
|
||||
type DB struct {
|
||||
dir string // where to store our cache of oui.csv
|
||||
ouiURL string // can be overridden for testing
|
||||
|
||||
sync.Mutex
|
||||
cond *sync.Cond
|
||||
loaded bool
|
||||
err error
|
||||
|
||||
// orgs is a map from assignment (e.g. f0:9f:c2) to organization name
|
||||
// (e.g. Ubiquiti Networks Inc.), gathered from the IEEE MA-L (MAC Address
|
||||
// Block Large, formerly known as OUI):
|
||||
// https://regauth.standards.ieee.org/standards-ra-web/pub/view.html#registries
|
||||
orgs map[string]string
|
||||
}
|
||||
|
||||
// NewDB loads a database from the cached version in dir, if any, and
|
||||
// asynchronously triggers an update. Use WaitUntilLoaded() to ensure Lookup()
|
||||
// will work, or use Lookup() opportunistically at any time.
|
||||
func NewDB(dir string) *DB {
|
||||
db := &DB{
|
||||
dir: dir,
|
||||
ouiURL: "http://standards-oui.ieee.org/oui/oui.csv",
|
||||
}
|
||||
db.cond = sync.NewCond(&db.Mutex)
|
||||
go db.update()
|
||||
return db
|
||||
}
|
||||
|
||||
// Lookup returns the organization name for the specified assignment, if
|
||||
// found. Assignment is a large MAC address block assignment, e.g. f0:9f:c2.
|
||||
func (d *DB) Lookup(assignment string) string {
|
||||
d.Lock()
|
||||
defer d.Unlock()
|
||||
return d.orgs[assignment]
|
||||
}
|
||||
|
||||
// WaitUntilLoaded blocks until the database was loaded.
|
||||
func (d *DB) WaitUntilLoaded() error {
|
||||
d.Lock()
|
||||
defer d.Unlock()
|
||||
for !d.loaded {
|
||||
d.cond.Wait()
|
||||
}
|
||||
return d.err
|
||||
}
|
||||
|
||||
func (d *DB) setErr(err error) {
|
||||
d.Lock()
|
||||
defer d.Unlock()
|
||||
d.loaded = true
|
||||
d.cond.Broadcast()
|
||||
d.err = err
|
||||
}
|
||||
|
||||
func (d *DB) update() {
|
||||
req, err := http.NewRequest("GET", d.ouiURL, nil)
|
||||
if err != nil {
|
||||
d.setErr(err)
|
||||
return
|
||||
}
|
||||
|
||||
csvPath := filepath.Join(d.dir, "oui.csv")
|
||||
// If any version exists, load it so that lookups work ASAP:
|
||||
if f, err := os.Open(csvPath); err == nil {
|
||||
if st, err := f.Stat(); err == nil {
|
||||
req.Header.Set("If-Modified-Since", st.ModTime().UTC().Format(http.TimeFormat))
|
||||
}
|
||||
defer f.Close()
|
||||
if err := d.load(f); err != nil {
|
||||
// Force a re-download in case our file is corrupted:
|
||||
req.Header.Del("If-Modified-Since")
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
d.setErr(err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode == http.StatusNotModified {
|
||||
d.setErr(nil)
|
||||
return // already up-to-date
|
||||
}
|
||||
if got, want := resp.StatusCode, http.StatusOK; got != want {
|
||||
body, _ := ioutil.ReadAll(resp.Body)
|
||||
d.setErr(fmt.Errorf("%s: unexpected HTTP status: got %v, want %v (%v)", d.ouiURL, resp.Status, want, body))
|
||||
return
|
||||
}
|
||||
if err := os.MkdirAll(d.dir, 0755); err != nil {
|
||||
d.setErr(err)
|
||||
return
|
||||
}
|
||||
f, err := renameio.TempFile(d.dir, csvPath)
|
||||
if err != nil {
|
||||
d.setErr(err)
|
||||
return
|
||||
}
|
||||
defer f.Cleanup()
|
||||
if _, err := io.Copy(f, resp.Body); err != nil {
|
||||
d.setErr(err)
|
||||
return
|
||||
}
|
||||
if t, err := http.ParseTime(resp.Header.Get("Last-Modified")); err == nil {
|
||||
if err := os.Chtimes(f.Name(), t, t); err != nil {
|
||||
log.Print(err)
|
||||
}
|
||||
}
|
||||
if err := f.CloseAtomicallyReplace(); err != nil {
|
||||
d.setErr(err)
|
||||
return
|
||||
}
|
||||
{
|
||||
f, err := os.Open(csvPath)
|
||||
if err != nil {
|
||||
d.setErr(err)
|
||||
return
|
||||
}
|
||||
defer f.Close()
|
||||
d.setErr(d.load(f))
|
||||
}
|
||||
}
|
||||
|
||||
func (d *DB) load(r io.Reader) error {
|
||||
// As of 2019-01, we’re talking < 30000 records.
|
||||
records, err := csv.NewReader(r).ReadAll()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
orgs := make(map[string]string, len(records))
|
||||
var buf [3]byte
|
||||
for _, record := range records[1:] {
|
||||
assignment, org := record[1], record[2]
|
||||
n, err := hex.Decode(buf[:], []byte(assignment))
|
||||
if err != nil {
|
||||
return fmt.Errorf("hex.Decode(%s): %v", assignment, err)
|
||||
}
|
||||
if got, want := n, 3; got != want {
|
||||
return fmt.Errorf("decode assignment %q: got %d bytes, want %d bytes", assignment, got, want)
|
||||
}
|
||||
orgs[fmt.Sprintf("%02x:%02x:%02x", buf[0], buf[1], buf[2])] = org
|
||||
}
|
||||
d.Lock()
|
||||
defer d.Unlock()
|
||||
d.orgs = orgs
|
||||
d.loaded = true
|
||||
d.cond.Broadcast()
|
||||
return nil
|
||||
}
|
146
internal/oui/oui_test.go
Normal file
146
internal/oui/oui_test.go
Normal file
@ -0,0 +1,146 @@
|
||||
package oui
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDB(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
const (
|
||||
ubiquitiBlock = "f0:9f:c2"
|
||||
salcompBlock = "44:09:b8"
|
||||
)
|
||||
|
||||
tmpdir, err := ioutil.TempDir("", "oui")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.RemoveAll(tmpdir)
|
||||
|
||||
t.Run("FromScratch", func(t *testing.T) {
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Last-Modified", "Sun, 06 Jan 2019 15:03:46 GMT")
|
||||
io.WriteString(w, `Registry,Assignment,Organization Name,Organization Address
|
||||
MA-L,F09FC2,Ubiquiti Networks Inc.,2580 Orchard Parkway San Jose CA US 95131
|
||||
MA-L,4409B8,"Salcomp (Shenzhen) CO., LTD.","Salcomp Road, Furong Industrial Area, Xinqiao, Shajing, Baoan District Shenzhen Guangdong CN 518125 "
|
||||
`)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
db := NewDB(tmpdir)
|
||||
db.ouiURL = srv.URL
|
||||
if err := db.WaitUntilLoaded(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if got, want := db.Lookup(ubiquitiBlock), "Ubiquiti Networks Inc."; got != want {
|
||||
t.Errorf("db.Lookup(%q) = %v, want %v", ubiquitiBlock, got, want)
|
||||
}
|
||||
|
||||
if got, want := db.Lookup(salcompBlock), "Salcomp (Shenzhen) CO., LTD."; got != want {
|
||||
t.Errorf("db.Lookup(%q) = %v, want %v", salcompBlock, got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("BrokenUpstream", func(t *testing.T) {
|
||||
unblock := make(chan struct{})
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
<-unblock
|
||||
http.Error(w, "yuck!", http.StatusInternalServerError)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
db := NewDB(tmpdir)
|
||||
db.ouiURL = srv.URL
|
||||
if err := db.WaitUntilLoaded(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if got, want := db.Lookup(ubiquitiBlock), "Ubiquiti Networks Inc."; got != want {
|
||||
t.Errorf("db.Lookup(%q) = %v, want %v", ubiquitiBlock, got, want)
|
||||
}
|
||||
|
||||
db.Lock()
|
||||
db.loaded = false // reset so that we can wait again
|
||||
db.Unlock()
|
||||
unblock <- struct{}{}
|
||||
if err := db.WaitUntilLoaded(); err == nil {
|
||||
t.Fatal("db.WaitUntilLoaded returned no error despite HTTP 500")
|
||||
}
|
||||
|
||||
if got, want := db.Lookup(ubiquitiBlock), "Ubiquiti Networks Inc."; got != want {
|
||||
t.Errorf("db.Lookup(%q) = %v, want %v", ubiquitiBlock, got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("NoUpdates", func(t *testing.T) {
|
||||
unblock := make(chan struct{})
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
<-unblock
|
||||
w.WriteHeader(http.StatusNotModified)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
db := NewDB(tmpdir)
|
||||
db.ouiURL = srv.URL
|
||||
if err := db.WaitUntilLoaded(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if got, want := db.Lookup(ubiquitiBlock), "Ubiquiti Networks Inc."; got != want {
|
||||
t.Errorf("db.Lookup(%q) = %v, want %v", ubiquitiBlock, got, want)
|
||||
}
|
||||
|
||||
db.Lock()
|
||||
db.loaded = false // reset so that we can wait again
|
||||
db.Unlock()
|
||||
unblock <- struct{}{}
|
||||
if err := db.WaitUntilLoaded(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if got, want := db.Lookup(ubiquitiBlock), "Ubiquiti Networks Inc."; got != want {
|
||||
t.Errorf("db.Lookup(%q) = %v, want %v", ubiquitiBlock, got, want)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Update", func(t *testing.T) {
|
||||
unblock := make(chan struct{})
|
||||
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
<-unblock
|
||||
w.Header().Set("Last-Modified", "Sun, 06 Jan 2019 15:03:49 GMT")
|
||||
io.WriteString(w, `Registry,Assignment,Organization Name,Organization Address
|
||||
MA-L,F09FC2,Obiquiti Networks Inc.,2580 Orchard Parkway San Jose CA US 95131
|
||||
`)
|
||||
}))
|
||||
defer srv.Close()
|
||||
|
||||
db := NewDB(tmpdir)
|
||||
db.ouiURL = srv.URL
|
||||
if err := db.WaitUntilLoaded(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if got, want := db.Lookup(ubiquitiBlock), "Ubiquiti Networks Inc."; got != want {
|
||||
t.Errorf("db.Lookup(%q) = %v, want %v", ubiquitiBlock, got, want)
|
||||
}
|
||||
|
||||
db.Lock()
|
||||
db.loaded = false // reset so that we can wait again
|
||||
db.Unlock()
|
||||
unblock <- struct{}{}
|
||||
if err := db.WaitUntilLoaded(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if got, want := db.Lookup(ubiquitiBlock), "Obiquiti Networks Inc."; got != want {
|
||||
t.Errorf("db.Lookup(%q) = %v, want %v", ubiquitiBlock, got, want)
|
||||
}
|
||||
})
|
||||
}
|
Loading…
x
Reference in New Issue
Block a user