Files
wlc-9800-tools/main.go

384 lines
9.8 KiB
Go

package main
import (
"crypto/tls"
"database/sql"
"encoding/json"
"flag"
"fmt"
"io"
"log"
"net/http"
"strings"
"time"
_ "github.com/mattn/go-sqlite3"
)
type ClientOperData struct {
CiscoIOSXEWirelessClientOper struct {
CommonOperData []struct {
ClientMac string `json:"client-mac"`
ApName string `json:"ap-name"`
WlanID int `json:"wlan-id"`
} `json:"common-oper-data"`
Dot11OperData []struct {
ClientMac string `json:"ms-mac-address"`
VapSSID string `json:"vap-ssid"`
} `json:"dot11-oper-data"`
DCInfo []struct {
ClientMac string `json:"client-mac"`
DeviceType string `json:"device-type"`
DayZeroDC string `json:"day-zero-dc"`
DeviceName string `json:"device-name"`
DeviceVendor string `json:"device-vendor"`
} `json:"dc-info"`
} `json:"Cisco-IOS-XE-wireless-client-oper:client-oper-data"`
}
func isAndroidDevice(mac string) (bool, string) {
macUpper := strings.ToUpper(mac)
parts := strings.Split(macUpper, ":")
if len(parts) < 3 {
return false, ""
}
oui := strings.Join(parts[:3], ":")
if manufacturer, exists := androidManufacturerOUIs[strings.ToLower(oui)]; exists {
return true, manufacturer
}
return false, ""
}
func main() {
showFlag := flag.Bool("show", false, "Show stored clients")
androidOnlyFlag := flag.Bool("android", false, "Show only Android devices (use with -show)")
ouiFilterFlag := flag.Bool("oui", false, "Filter by Android manufacturer OUI when fetching")
apFilterFlag := flag.String("ap", "", "Filter by AP name (use with -show)")
ssidFilterFlag := flag.String("ssid", "", "Filter by SSID (use with -show)")
flag.Parse()
db, err := initDB()
if err != nil {
log.Fatal("Failed to initialize database:", err)
}
defer db.Close()
if *showFlag {
if err := showClients(db, *androidOnlyFlag, *apFilterFlag, *ssidFilterFlag); err != nil {
log.Fatal("Failed to show clients:", err)
}
return
}
data, err := fetchClientData()
if err != nil {
log.Fatal("Failed to fetch client data:", err)
}
var clientsToSave []map[string]string
if *ouiFilterFlag {
clientsToSave = filterByOUI(data)
fmt.Printf("Filtered %d Android devices by OUI\n", len(clientsToSave))
} else {
clientsToSave = getAllClients(data)
}
if err := saveToDatabase(db, clientsToSave); err != nil {
log.Fatal("Failed to save to database:", err)
}
fmt.Printf("Successfully saved %d clients to database\n", len(clientsToSave))
}
func initDB() (*sql.DB, error) {
db, err := sql.Open("sqlite3", "./clients.db")
if err != nil {
return nil, err
}
createTableSQL := `CREATE TABLE IF NOT EXISTS android_clients (
id INTEGER PRIMARY KEY AUTOINCREMENT,
client_mac TEXT NOT NULL,
ap_name TEXT,
ssid TEXT,
device_type TEXT,
day_zero_dc TEXT,
device_name TEXT,
device_vendor TEXT,
timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
);`
_, err = db.Exec(createTableSQL)
if err != nil {
return nil, err
}
return db, nil
}
func fetchClientData() (*ClientOperData, error) {
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client := &http.Client{
Transport: tr,
Timeout: 60 * time.Second,
}
req, err := http.NewRequest("GET", "https://10.0.3.4/restconf/data/Cisco-IOS-XE-wireless-client-oper:client-oper-data", nil)
if err != nil {
return nil, err
}
req.SetBasicAuth("othman.suseno", "Pnd77net!")
req.Header.Set("Accept", "application/yang-data+json")
fmt.Println("Fetching data from Cisco API...")
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("request failed: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("API returned status code: %d", resp.StatusCode)
}
fmt.Println("Reading response...")
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response: %w", err)
}
fmt.Printf("Received %d bytes, parsing JSON...\n", len(body))
var data ClientOperData
if err := json.Unmarshal(body, &data); err != nil {
return nil, fmt.Errorf("failed to parse JSON: %w", err)
}
return &data, nil
}
func getAllClients(data *ClientOperData) []map[string]string {
var allClients []map[string]string
dcInfoMap := make(map[string]map[string]string)
for _, dc := range data.CiscoIOSXEWirelessClientOper.DCInfo {
dcInfoMap[dc.ClientMac] = map[string]string{
"device_type": dc.DeviceType,
"day_zero_dc": dc.DayZeroDC,
"device_name": dc.DeviceName,
"device_vendor": dc.DeviceVendor,
}
}
dot11InfoMap := make(map[string]string)
for _, dot11 := range data.CiscoIOSXEWirelessClientOper.Dot11OperData {
dot11InfoMap[dot11.ClientMac] = dot11.VapSSID
}
fmt.Printf("Found %d clients with device classification info\n", len(dcInfoMap))
fmt.Printf("Found %d clients with SSID info\n", len(dot11InfoMap))
fmt.Printf("Found %d total clients in common-oper-data\n", len(data.CiscoIOSXEWirelessClientOper.CommonOperData))
for i, client := range data.CiscoIOSXEWirelessClientOper.CommonOperData {
dcInfo, hasDCInfo := dcInfoMap[client.ClientMac]
ssid, hasSSID := dot11InfoMap[client.ClientMac]
if i < 3 {
fmt.Printf("Sample client %d: MAC=%s, AP=%s, SSID=%s\n",
i+1, client.ClientMac, client.ApName, ssid)
}
clientData := map[string]string{
"client_mac": client.ClientMac,
"ap_name": client.ApName,
"ssid": "",
"device_type": "",
"day_zero_dc": "",
"device_name": "",
"device_vendor": "",
}
if hasSSID {
clientData["ssid"] = ssid
}
if hasDCInfo {
clientData["device_type"] = dcInfo["device_type"]
clientData["day_zero_dc"] = dcInfo["day_zero_dc"]
clientData["device_name"] = dcInfo["device_name"]
clientData["device_vendor"] = dcInfo["device_vendor"]
}
allClients = append(allClients, clientData)
}
return allClients
}
func filterByOUI(data *ClientOperData) []map[string]string {
var androidClients []map[string]string
dcInfoMap := make(map[string]map[string]string)
for _, dc := range data.CiscoIOSXEWirelessClientOper.DCInfo {
dcInfoMap[dc.ClientMac] = map[string]string{
"device_type": dc.DeviceType,
"day_zero_dc": dc.DayZeroDC,
"device_name": dc.DeviceName,
"device_vendor": dc.DeviceVendor,
}
}
dot11InfoMap := make(map[string]string)
for _, dot11 := range data.CiscoIOSXEWirelessClientOper.Dot11OperData {
dot11InfoMap[dot11.ClientMac] = dot11.VapSSID
}
androidCount := 0
for _, client := range data.CiscoIOSXEWirelessClientOper.CommonOperData {
isAndroid, manufacturer := isAndroidDevice(client.ClientMac)
if isAndroid {
dcInfo, hasDCInfo := dcInfoMap[client.ClientMac]
ssid, hasSSID := dot11InfoMap[client.ClientMac]
clientData := map[string]string{
"client_mac": client.ClientMac,
"ap_name": client.ApName,
"ssid": "",
"device_type": "",
"day_zero_dc": "",
"device_name": "",
"device_vendor": manufacturer,
}
if hasSSID {
clientData["ssid"] = ssid
}
if hasDCInfo {
clientData["device_type"] = dcInfo["device_type"]
clientData["day_zero_dc"] = dcInfo["day_zero_dc"]
clientData["device_name"] = dcInfo["device_name"]
if dcInfo["device_vendor"] != "" {
clientData["device_vendor"] = dcInfo["device_vendor"]
}
}
androidClients = append(androidClients, clientData)
androidCount++
if androidCount <= 5 {
fmt.Printf("Found Android device: MAC=%s, AP=%s, SSID=%s, Manufacturer=%s\n",
client.ClientMac, client.ApName, clientData["ssid"], manufacturer)
}
}
}
return androidClients
}
func saveToDatabase(db *sql.DB, clients []map[string]string) error {
tx, err := db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
stmt, err := tx.Prepare(`INSERT INTO android_clients
(client_mac, ap_name, ssid, device_type, day_zero_dc, device_name, device_vendor)
VALUES (?, ?, ?, ?, ?, ?, ?)`)
if err != nil {
return err
}
defer stmt.Close()
for _, client := range clients {
_, err = stmt.Exec(
client["client_mac"],
client["ap_name"],
client["ssid"],
client["device_type"],
client["day_zero_dc"],
client["device_name"],
client["device_vendor"],
)
if err != nil {
return err
}
}
return tx.Commit()
}
func showClients(db *sql.DB, androidOnly bool, apFilter string, ssidFilter string) error {
query := "SELECT client_mac, ap_name, ssid, device_type, day_zero_dc, device_name, device_vendor FROM android_clients"
var args []interface{}
var conditions []string
if apFilter != "" {
conditions = append(conditions, "ap_name LIKE ?")
args = append(args, "%"+apFilter+"%")
}
if ssidFilter != "" {
conditions = append(conditions, "ssid LIKE ?")
args = append(args, "%"+ssidFilter+"%")
}
if len(conditions) > 0 {
query += " WHERE " + strings.Join(conditions, " AND ")
}
rows, err := db.Query(query, args...)
if err != nil {
return err
}
defer rows.Close()
fmt.Println("\n=== Stored Clients ===")
fmt.Printf("%-20s %-20s %-20s %-15s %-15s %-30s %-20s\n",
"MAC Address", "AP Name", "SSID", "Device Type", "Day Zero DC", "Device Name", "Device Vendor")
fmt.Println(strings.Repeat("-", 140))
count := 0
androidCount := 0
for rows.Next() {
var mac, apName, ssid, deviceType, dayZeroDC, deviceName, deviceVendor string
if err := rows.Scan(&mac, &apName, &ssid, &deviceType, &dayZeroDC, &deviceName, &deviceVendor); err != nil {
return err
}
isAndroid, manufacturer := isAndroidDevice(mac)
if androidOnly && !isAndroid {
continue
}
if isAndroid {
androidCount++
if deviceVendor == "" {
deviceVendor = manufacturer
}
}
fmt.Printf("%-20s %-20s %-20s %-15s %-15s %-30s %-20s\n",
mac, apName, ssid, deviceType, dayZeroDC, deviceName, deviceVendor)
count++
}
fmt.Println(strings.Repeat("-", 140))
if androidOnly {
fmt.Printf("Total Android devices: %d\n", count)
} else {
fmt.Printf("Total clients: %d (Android: %d)\n", count, androidCount)
}
return rows.Err()
}