Files
netperfect/main.go
2020-07-26 18:47:38 +00:00

250 lines
4.4 KiB
Go

package main
import (
"context"
"crypto/tls"
"encoding/binary"
"fmt"
"log"
"net"
"net/http"
"strings"
"sync"
"time"
"github.com/mdlayher/ethernet"
"github.com/mdlayher/lldp"
"github.com/mdlayher/raw"
"github.com/vishvananda/netlink"
)
func main() {
link, err := getLink()
if err != nil {
log.Fatal(err)
}
log.Printf("Network interface: %s", link.Attrs().Name)
upstream, err := getUpstream(link.Attrs().Name)
if err != nil {
log.Fatal(err)
}
log.Printf("Connected to %s on %s", upstream.Name, upstream.Port)
api, err := findAPI(link)
if err != nil {
log.Fatal(err)
}
log.Printf("UniFi API URL: %s", api)
unifi, err := NewClient(api)
if err != nil {
log.Fatal(err)
}
err = unifi.Login()
if err != nil {
log.Fatal(err)
}
devices, err := unifi.ListDevices()
if err != nil {
log.Fatal(err)
}
upstream_dev := devices[upstream.Name]
if upstream_dev == nil {
log.Fatalf("Can't find my upstream in UniFi device list")
}
upstream_dev.PathHop([]*Device{}, map[string]*Device{})
upstream_dev.LogHop("", true)
}
type Upstream struct {
Name string
Port string
MAC string
}
func getUpstream(intName string) (*Upstream, error) {
intr, err := net.InterfaceByName(intName)
if err != nil {
return nil, err
}
sock, err := raw.ListenPacket(intr, 0x88cc /* LLDP ethertype */, nil)
if err != nil {
return nil, err
}
defer sock.Close()
buf := make([]byte, intr.MTU)
bufLen, addr, err := sock.ReadFrom(buf)
if err != nil {
return nil, err
}
ethFrame := &ethernet.Frame{}
err = ethFrame.UnmarshalBinary(buf[:bufLen])
if err != nil {
return nil, err
}
lldpFrame := &lldp.Frame{}
err = lldpFrame.UnmarshalBinary(ethFrame.Payload)
if err != nil {
return nil, err
}
ret := &Upstream {
MAC: strings.ToUpper(addr.String()),
}
for _, tlv := range lldpFrame.Optional {
switch tlv.Type {
case lldp.TLVTypePortDescription:
ret.Port = strings.ToLower(string(tlv.Value))
case lldp.TLVTypeSystemName:
ret.Name = string(tlv.Value)
}
}
return ret, nil
}
func findAPI(link netlink.Link) (string, error) {
addrs, err := netlink.AddrList(link, 2 /* AF_INET */)
if err != nil {
return "", err
}
apis := []string{}
for _, addr := range addrs {
ip := binary.BigEndian.Uint32(addr.IPNet.IP.To4())
mask := binary.BigEndian.Uint32(addr.IPNet.Mask)
start := ip & mask
end := ip | (^mask & 0xffffffff)
addr_apis, err := findAPIs(start, end)
if err != nil {
return "", err
}
apis = append(apis, addr_apis...)
}
if len(apis) == 0 {
return "", fmt.Errorf("no UniFi APIs found")
}
if len(apis) > 1 {
return "", fmt.Errorf("multiple APIs found TODO")
}
return apis[0], nil
}
func findAPIs(start_ip, end_ip uint32) ([]string, error) {
wg := sync.WaitGroup{}
wg.Add(int(end_ip - start_ip + 1))
ret := []string{}
for iter := start_ip; iter <= end_ip; iter++ {
go func(ip uint32) {
defer wg.Done()
ctx, cancel := context.WithTimeout(context.Background(), 500 * time.Millisecond)
defer cancel()
url := fmt.Sprintf(
"https://%d.%d.%d.%d",
ip >> 24 & 0xff,
ip >> 16 & 0xff,
ip >> 8 & 0xff,
ip >> 0 & 0xff,
)
req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil {
// TODO: should return an outer error
return
}
tr := &http.Transport{
TLSClientConfig: &tls.Config{
InsecureSkipVerify: true,
},
}
client := http.Client{
Transport: tr,
}
resp, err := client.Do(req)
if err != nil {
// Probably not https
return
}
set_cookie := resp.Header["Set-Cookie"]
if len(set_cookie) != 1 || !strings.HasPrefix(set_cookie[0], "TOKEN=") {
// Probably not Unifi
return
}
ret = append(ret, url)
}(iter)
}
wg.Wait()
return ret, nil
}
func getLink() (netlink.Link, error) {
links, err := getLinks()
if err != nil {
return nil, err
}
if len(links) == 0 {
return nil, fmt.Errorf("no links found (TODO)")
}
if len(links) > 1 {
return nil, fmt.Errorf("more than one link (TODO)")
}
return links[0], nil
}
func getLinks() ([]netlink.Link, error) {
links, err := netlink.LinkList()
if err != nil {
return nil, err
}
ret := []netlink.Link{}
for _, link := range links {
if link.Attrs().Flags & net.FlagUp == 0 {
// Link is down
continue
}
if link.Attrs().Flags & net.FlagLoopback != 0 {
// Link is loopback
continue
}
if link.Attrs().EncapType != "ether" {
// Not ethernet
continue
}
ret = append(ret, link)
}
return ret, nil
}