424 lines
11 KiB
Go
424 lines
11 KiB
Go
//go:build linux
|
|
// +build linux
|
|
|
|
package utils
|
|
|
|
import (
|
|
"bufio"
|
|
"bytes"
|
|
"context"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"k8s.io/klog/v2"
|
|
)
|
|
|
|
func ConnectWifi(ctx context.Context, ssid, password string) error {
|
|
if ssid == "" {
|
|
return errors.New("ssid is empty")
|
|
}
|
|
|
|
nmcli, err := findCommand(ctx, "nmcli")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
args := []string{
|
|
"d",
|
|
"wifi",
|
|
"connect",
|
|
ssid,
|
|
}
|
|
|
|
if password != "" {
|
|
args = append(args, "password", password)
|
|
}
|
|
|
|
cmd := exec.CommandContext(ctx, nmcli, args...)
|
|
cmd.Env = os.Environ()
|
|
output, err := cmd.CombinedOutput()
|
|
klog.Info(string(output))
|
|
|
|
if err != nil {
|
|
klog.Error("exec cmd error, ", err, ", nmcli", " ", strings.Join(args, " "))
|
|
return err
|
|
}
|
|
|
|
if strings.Contains(string(output), "Error") {
|
|
err = errors.New(string(output))
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func EnableWifi(ctx context.Context) error {
|
|
nmcli, err := findCommand(ctx, "nmcli")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cmd := exec.CommandContext(ctx, nmcli, "r", "wifi", "on")
|
|
cmd.Env = os.Environ()
|
|
output, err := cmd.CombinedOutput()
|
|
klog.Info(string(output))
|
|
|
|
if err != nil {
|
|
klog.Error("exec cmd error, ", err, ", nmcli r wifi on")
|
|
return err
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func GetWifiDevice(ctx context.Context) (map[string]Device, error) {
|
|
return deviceStatus(ctx, func(d *Device) bool { return d.Type == "wifi" })
|
|
}
|
|
|
|
func GetAllDevice(ctx context.Context) (map[string]Device, error) {
|
|
return deviceStatus(ctx, func(d *Device) bool {
|
|
managedByOthers := []string{"cali", "kube", "tun", "tailscale"}
|
|
for _, devPrefix := range managedByOthers {
|
|
if strings.HasPrefix(d.Name, devPrefix) {
|
|
return false
|
|
}
|
|
}
|
|
|
|
return true
|
|
})
|
|
}
|
|
|
|
func ManagedAllDevices(ctx context.Context) (map[string]Device, error) {
|
|
return deviceStatus(ctx, func(d *Device) bool {
|
|
managedByOthers := []string{"cali", "kube", "tun", "tailscale"}
|
|
for _, devPrefix := range managedByOthers {
|
|
if strings.HasPrefix(d.Name, devPrefix) {
|
|
return false
|
|
}
|
|
}
|
|
if d.State == "unmanaged" {
|
|
nmcli, err := findCommand(ctx, "nmcli")
|
|
if err != nil {
|
|
klog.Error("find nmcli error, ", err)
|
|
return false
|
|
}
|
|
|
|
cmd := exec.CommandContext(ctx, nmcli, "device", "set", d.Name, "managed", "yes")
|
|
cmd.Env = os.Environ()
|
|
output, err := cmd.CombinedOutput()
|
|
if err != nil {
|
|
klog.Error("exec cmd error, ", err, ", nmcli device set ", d.Name, " managed yes")
|
|
return false
|
|
}
|
|
if strings.Contains(string(output), "Error") {
|
|
err = errors.New(string(output))
|
|
klog.Error("exec cmd error, ", err, ", nmcli device set ", d.Name, " managed yes")
|
|
return false
|
|
}
|
|
}
|
|
return true
|
|
})
|
|
}
|
|
|
|
func deviceStatus(ctx context.Context, filter func(d *Device) bool) (map[string]Device, error) {
|
|
nmcli, err := findCommand(ctx, "nmcli")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
fields := []string{"DEVICE", "TYPE", "STATE", "CONNECTION"}
|
|
|
|
cmdArgs := []string{"-g", strings.Join(fields, ",")}
|
|
cmdArgs = append(cmdArgs, "device", "status")
|
|
|
|
cmd := exec.CommandContext(ctx, nmcli, cmdArgs...)
|
|
cmd.Env = os.Environ()
|
|
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to execute nmcli with args %+q: %w", cmdArgs, err)
|
|
}
|
|
|
|
parsedOutput, err := parseCmdOutput(output, len(fields))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("failed to parse nmcli output: %w", err)
|
|
}
|
|
|
|
statuss := make(map[string]Device)
|
|
for _, fields := range parsedOutput {
|
|
d := Device{
|
|
Name: fields[0],
|
|
Type: fields[1],
|
|
State: fields[2],
|
|
Connection: fields[3],
|
|
}
|
|
|
|
if filter == nil || filter(&d) {
|
|
err = showDeviceByNM(ctx, d.Name, &d)
|
|
if err != nil {
|
|
klog.Error("failed to get device details for ", d.Name, ": ", err)
|
|
continue
|
|
}
|
|
|
|
statuss[d.Name] = d
|
|
}
|
|
}
|
|
|
|
return statuss, nil
|
|
}
|
|
|
|
func parseCmdOutput(output []byte, expectedCountOfFields int) ([][]string, error) {
|
|
lines := bytes.FieldsFunc(output, func(c rune) bool { return c == '\n' || c == '\r' })
|
|
|
|
var recordLines [][]string
|
|
for i, line := range lines {
|
|
recordLine := splitBySeparator(":", string(line))
|
|
if len(recordLine) != expectedCountOfFields {
|
|
return nil, fmt.Errorf(
|
|
"line %d contains %d fields but should %d",
|
|
i, len(recordLine), expectedCountOfFields,
|
|
)
|
|
}
|
|
|
|
recordLines = append(recordLines, recordLine)
|
|
}
|
|
|
|
return recordLines, nil
|
|
}
|
|
|
|
func splitBySeparator(separator, line string) []string {
|
|
escape := `\`
|
|
tempEscapedSeparator := "\x00"
|
|
|
|
replacedEscape := strings.ReplaceAll(line, escape+separator, tempEscapedSeparator)
|
|
records := strings.Split(replacedEscape, separator)
|
|
|
|
for i, record := range records {
|
|
records[i] = strings.ReplaceAll(record, tempEscapedSeparator, separator)
|
|
}
|
|
|
|
return records
|
|
}
|
|
|
|
// command: nmcli device show <interface>
|
|
// output format:
|
|
// GENERAL.DEVICE: enp3s0
|
|
// GENERAL.TYPE: ethernet
|
|
// GENERAL.HWADDR: 34:5A:60:35:69:CC
|
|
// GENERAL.MTU: 1500
|
|
// GENERAL.STATE: 100 (connected)
|
|
// GENERAL.CONNECTION: Wired connection 1
|
|
// GENERAL.CON-PATH: /org/freedesktop/NetworkManager/ActiveConnection/1
|
|
// WIRED-PROPERTIES.CARRIER: on
|
|
// IP4.ADDRESS[1]: 192.168.31.145/24
|
|
// IP4.GATEWAY: 192.168.31.1
|
|
// IP4.ROUTE[1]: dst = 169.254.0.0/16, nh = 0.0.0.0, mt = 1000
|
|
// IP4.ROUTE[2]: dst = 192.168.31.0/24, nh = 0.0.0.0, mt = 100
|
|
// IP4.ROUTE[3]: dst = 0.0.0.0/0, nh = 192.168.31.1, mt = 100
|
|
// IP4.DNS[1]: 192.168.31.1
|
|
// IP6.ADDRESS[1]: 2408:8606:1800:1::d4a/128
|
|
// IP6.ADDRESS[2]: 2408:8606:1800:1:16c5:3fa5:ad66:f6d9/64
|
|
// IP6.ADDRESS[3]: 2408:8606:1800:1:16f4:a31b:b33f:26f2/64
|
|
// IP6.ADDRESS[4]: fe80::7272:12f8:6ef6:2a42/64
|
|
// IP6.GATEWAY: fe80::5aea:1fff:fe64:b5dc
|
|
// IP6.ROUTE[1]: dst = fe80::/64, nh = ::, mt = 1024
|
|
// IP6.ROUTE[2]: dst = 2408:8606:1800:1::/64, nh = ::, mt = 100
|
|
// IP6.ROUTE[3]: dst = ::/0, nh = fe80::5aea:1fff:fe64:b5dc, mt = 20100
|
|
// IP6.ROUTE[4]: dst = 2408:8606:1800:1::d4a/128, nh = ::, mt = 100
|
|
// IP6.DNS[1]: fe80::5aea:1fff:fe64:b5dc
|
|
func showDeviceByNM(ctx context.Context, deviceName string, device *Device) error {
|
|
nmcli, err := findCommand(ctx, "nmcli")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cmd := exec.CommandContext(ctx, nmcli, "device", "show", deviceName)
|
|
cmd.Env = os.Environ()
|
|
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to execute nmcli: %w", err)
|
|
}
|
|
|
|
lines := bytes.Split(output, []byte("\n"))
|
|
for _, line := range lines {
|
|
if len(line) == 0 {
|
|
continue
|
|
}
|
|
fields := bytes.SplitN(line, []byte(":"), 2)
|
|
if len(fields) < 2 {
|
|
continue
|
|
}
|
|
key := strings.TrimSpace(string(fields[0]))
|
|
value := strings.TrimSpace(string(fields[1]))
|
|
|
|
switch key {
|
|
case "IP4.ADDRESS[1]":
|
|
ipAndMask := strings.Split(value, "/")
|
|
if len(ipAndMask) > 1 {
|
|
device.Ipv4Address = ipAndMask[0]
|
|
cidr, err := strconv.Atoi(ipAndMask[1])
|
|
if err != nil {
|
|
klog.Error("convert cidr error, ", err)
|
|
continue
|
|
}
|
|
mask, err := MaskFromCIDR(cidr)
|
|
if err != nil {
|
|
klog.Error("get mask from cidr error, ", err)
|
|
continue
|
|
}
|
|
device.Ipv4Mask = mask
|
|
}
|
|
case "IP4.GATEWAY":
|
|
device.Ipv4Gateway = value
|
|
case "IP4.DNS[1]":
|
|
device.Ipv4DNS = value
|
|
case "IP6.ADDRESS[1]":
|
|
device.Ipv6Address = value
|
|
case "IP6.GATEWAY":
|
|
device.Ipv6Gateway = value
|
|
case "IP6.DNS[1]":
|
|
device.Ipv6DNS = value
|
|
case "GENERAL.CONNECTION":
|
|
err := showConnectionByNM(ctx, value, device)
|
|
if err != nil {
|
|
klog.V(8).Info("get connection method error, ", err, ", connection name: ", value)
|
|
}
|
|
default:
|
|
continue
|
|
}
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
// nmcli connection show "Wired connection 1"
|
|
func showConnectionByNM(ctx context.Context, connectionName string, device *Device) error {
|
|
nmcli, err := findCommand(ctx, "nmcli")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cmd := exec.CommandContext(ctx, nmcli, "connection", "show", connectionName)
|
|
cmd.Env = os.Environ()
|
|
|
|
output, err := cmd.Output()
|
|
if err != nil {
|
|
return fmt.Errorf("failed to execute nmcli: %w", err)
|
|
}
|
|
|
|
lines := bytes.Split(output, []byte("\n"))
|
|
for _, line := range lines {
|
|
if len(line) == 0 {
|
|
continue
|
|
}
|
|
fields := bytes.SplitN(line, []byte(":"), 2)
|
|
if len(fields) < 2 {
|
|
continue
|
|
}
|
|
key := strings.TrimSpace(string(fields[0]))
|
|
value := strings.TrimSpace(string(fields[1]))
|
|
|
|
switch key {
|
|
case "ipv4.method":
|
|
device.Method = value
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
type NetworkTraffic struct {
|
|
Interface string
|
|
RxBytes uint64
|
|
TxBytes uint64
|
|
}
|
|
|
|
func getInterfaceTraffic() (traffic map[string]*NetworkTraffic, err error) {
|
|
file, err := os.Open("/proc/net/dev")
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer file.Close()
|
|
|
|
scanner := bufio.NewScanner(file)
|
|
traffic = make(map[string]*NetworkTraffic)
|
|
for scanner.Scan() {
|
|
line := strings.TrimSpace(scanner.Text())
|
|
if !strings.Contains(line, ":") {
|
|
continue
|
|
}
|
|
parts := strings.SplitN(line, ":", 2)
|
|
fields := strings.Fields(parts[1])
|
|
if len(fields) < 9 {
|
|
return nil, fmt.Errorf("unexpected format for interface %s", parts[0])
|
|
}
|
|
rxBytes, err := strconv.ParseUint(fields[0], 10, 64)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
txBytes, err := strconv.ParseUint(fields[8], 10, 64)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
traffic[parts[0]] = &NetworkTraffic{
|
|
Interface: parts[0],
|
|
RxBytes: rxBytes,
|
|
TxBytes: txBytes,
|
|
}
|
|
}
|
|
|
|
return traffic, nil
|
|
}
|
|
|
|
type NetworkTrafficRate struct {
|
|
Interface string
|
|
RxBytes uint64
|
|
TxBytes uint64
|
|
RxRate float64
|
|
TxRate float64
|
|
UpdateTime time.Time
|
|
}
|
|
|
|
var AllNetworkDeviceTraffic = make(map[string]*NetworkTrafficRate)
|
|
|
|
func UpdateNetworkTraffic(ctx context.Context) {
|
|
traffic, err := getInterfaceTraffic()
|
|
if err != nil {
|
|
klog.Error("get interface traffic error, ", err)
|
|
return
|
|
}
|
|
|
|
for name, netTraffic := range traffic {
|
|
rate, ok := AllNetworkDeviceTraffic[name]
|
|
if !ok {
|
|
AllNetworkDeviceTraffic[name] = &NetworkTrafficRate{
|
|
Interface: name,
|
|
RxBytes: netTraffic.RxBytes,
|
|
TxBytes: netTraffic.TxBytes,
|
|
UpdateTime: time.Now(),
|
|
}
|
|
continue
|
|
}
|
|
|
|
rate.RxRate = float64(netTraffic.RxBytes-rate.RxBytes) / time.Since(rate.UpdateTime).Seconds()
|
|
rate.TxRate = float64(netTraffic.TxBytes-rate.TxBytes) / time.Since(rate.UpdateTime).Seconds()
|
|
rate.RxBytes = netTraffic.RxBytes
|
|
rate.TxBytes = netTraffic.TxBytes
|
|
rate.UpdateTime = time.Now()
|
|
}
|
|
}
|
|
|
|
func GetInterfaceTraffic(iface string) (rxBytes, txBytes float64, err error) {
|
|
rates, ok := AllNetworkDeviceTraffic[iface]
|
|
if !ok {
|
|
return 0, 0, fmt.Errorf("interface %s not found", iface)
|
|
}
|
|
return rates.RxRate, rates.TxRate, nil
|
|
}
|