diff --git a/main.go b/main.go new file mode 100644 index 0000000..f034913 --- /dev/null +++ b/main.go @@ -0,0 +1,226 @@ +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) + + api, err := findApi(link) + if err != nil { + log.Fatal(err) + } + log.Printf("UniFi API URL: %s", api) + + upstream, err := getUpstream(link.Attrs().Name) + if err != nil { + log.Fatal(err) + } + log.Printf("Connected to %s [%s] on %s", upstream.device, upstream.addr, upstream.port) +} + +type upstream struct { + device string + port string + addr net.Addr +} + +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 := ðernet.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 { + addr: addr, + } + + for _, tlv := range lldpFrame.Optional { + switch tlv.Type { + case lldp.TLVTypePortDescription: + ret.port = strings.ToLower(string(tlv.Value)) + case lldp.TLVTypeSystemName: + ret.device = 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 +}