| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892 |
- package bridge
- import (
- "bufio"
- "encoding/json"
- "fmt"
- "net"
- "os"
- "os/exec"
- "path/filepath"
- "sort"
- "strconv"
- "strings"
- "sync"
- "time"
- "github.com/vishvananda/netlink"
- )
- const BridgeConfigFile = "/etc/proxmox-router/bridge.json"
- type BridgeConfigStore struct {
- Bridges []*BridgeInstance `json:"bridges"`
- Ports []*BridgePort `json:"ports"`
- VLANs []*BridgeVLAN `json:"vlans"`
- }
- type BridgeManager struct {
- mu sync.RWMutex
- storePath string
- bridges map[string]*BridgeInstance
- ports map[string]*BridgePort
- vlans map[string]*BridgeVLAN
- }
- func NewBridgeManager() *BridgeManager {
- bm := &BridgeManager{
- storePath: BridgeConfigFile,
- bridges: make(map[string]*BridgeInstance),
- ports: make(map[string]*BridgePort),
- vlans: make(map[string]*BridgeVLAN),
- }
- bm.load()
- bm.SyncAll()
- return bm
- }
- func (m *BridgeManager) load() {
- m.mu.Lock()
- defer m.mu.Unlock()
- data, err := os.ReadFile(m.storePath)
- if err != nil {
- return
- }
- var store BridgeConfigStore
- if err := json.Unmarshal(data, &store); err != nil {
- return
- }
- for _, b := range store.Bridges {
- m.bridges[b.ID] = b
- }
- for _, p := range store.Ports {
- m.ports[p.ID] = p
- }
- for _, v := range store.VLANs {
- m.vlans[v.ID] = v
- }
- }
- func (m *BridgeManager) save() error {
- _ = os.MkdirAll(filepath.Dir(m.storePath), 0755)
- store := BridgeConfigStore{
- Bridges: make([]*BridgeInstance, 0, len(m.bridges)),
- Ports: make([]*BridgePort, 0, len(m.ports)),
- VLANs: make([]*BridgeVLAN, 0, len(m.vlans)),
- }
- for _, b := range m.bridges {
- store.Bridges = append(store.Bridges, b)
- }
- for _, p := range m.ports {
- store.Ports = append(store.Ports, p)
- }
- for _, v := range m.vlans {
- store.VLANs = append(store.VLANs, v)
- }
- data, err := json.MarshalIndent(store, "", " ")
- if err != nil {
- return err
- }
- return os.WriteFile(m.storePath, data, 0644)
- }
- // ==================== Bridge Interfaces ====================
- func (m *BridgeManager) ListBridges() []*BridgeInstance {
- m.mu.Lock()
- defer m.mu.Unlock()
- list := make([]*BridgeInstance, 0, len(m.bridges))
- for _, b := range m.bridges {
- m.updateBridgeLiveStats(b)
- list = append(list, b)
- }
- sort.Slice(list, func(i, j int) bool {
- return list[i].Name < list[j].Name
- })
- return list
- }
- func (m *BridgeManager) GetBridge(idOrName string) (*BridgeInstance, bool) {
- m.mu.Lock()
- defer m.mu.Unlock()
- if b, ok := m.bridges[idOrName]; ok {
- m.updateBridgeLiveStats(b)
- return b, true
- }
- for _, b := range m.bridges {
- if b.Name == idOrName {
- m.updateBridgeLiveStats(b)
- return b, true
- }
- }
- return nil, false
- }
- func (m *BridgeManager) SaveBridge(b BridgeInstance) error {
- m.mu.Lock()
- defer m.mu.Unlock()
- if b.Name == "" {
- return fmt.Errorf("bridge name is required")
- }
- if b.MTU <= 0 {
- b.MTU = 1500
- }
- if b.PVID <= 0 {
- b.PVID = 1
- }
- if b.STP == "" {
- b.STP = "rstp"
- }
- if b.Priority <= 0 {
- b.Priority = 32768
- }
- if b.ID == "" {
- b.ID = fmt.Sprintf("br-%d", time.Now().UnixNano())
- }
- // Update existing or add new
- m.bridges[b.ID] = &b
- _ = m.save()
- // Apply in Linux kernel
- return m.syncBridge(&b)
- }
- func (m *BridgeManager) DeleteBridge(id string) error {
- m.mu.Lock()
- defer m.mu.Unlock()
- b, ok := m.bridges[id]
- if !ok {
- return fmt.Errorf("bridge not found")
- }
- // Detach any ports belonging to this bridge
- for _, p := range m.ports {
- if p.Bridge == b.Name {
- _ = exec.Command("ip", "link", "set", p.Interface, "nomaster").Run()
- delete(m.ports, p.ID)
- }
- }
- // Delete any VLAN entries for this bridge
- for vid, v := range m.vlans {
- if v.Bridge == b.Name {
- delete(m.vlans, vid)
- }
- }
- // Delete bridge link in kernel
- _ = exec.Command("ip", "link", "del", b.Name).Run()
- delete(m.bridges, id)
- return m.save()
- }
- func (m *BridgeManager) SetBridgeState(id string, disabled bool) error {
- m.mu.Lock()
- defer m.mu.Unlock()
- b, ok := m.bridges[id]
- if !ok {
- return fmt.Errorf("bridge not found")
- }
- b.Disabled = disabled
- _ = m.save()
- if disabled {
- _ = exec.Command("ip", "link", "set", b.Name, "down").Run()
- } else {
- _ = exec.Command("ip", "link", "set", b.Name, "up").Run()
- }
- return nil
- }
- func (m *BridgeManager) AddBridgeIP(idOrName string, ipCIDR string) error {
- m.mu.Lock()
- defer m.mu.Unlock()
- b, ok := m.bridges[idOrName]
- if !ok {
- for _, br := range m.bridges {
- if br.Name == idOrName {
- b = br
- ok = true
- break
- }
- }
- }
- if !ok {
- return fmt.Errorf("bridge not found")
- }
- ipCIDR = strings.TrimSpace(ipCIDR)
- if _, _, err := net.ParseCIDR(ipCIDR); err != nil {
- return fmt.Errorf("invalid IP/CIDR: %w", err)
- }
- // Check if already in list
- for _, ip := range b.IPAddresses {
- if ip == ipCIDR {
- return nil
- }
- }
- b.IPAddresses = append(b.IPAddresses, ipCIDR)
- _ = m.save()
- // Add to kernel
- _ = exec.Command("ip", "addr", "add", ipCIDR, "dev", b.Name).Run()
- return nil
- }
- func (m *BridgeManager) DeleteBridgeIP(idOrName string, ipCIDR string) error {
- m.mu.Lock()
- defer m.mu.Unlock()
- b, ok := m.bridges[idOrName]
- if !ok {
- for _, br := range m.bridges {
- if br.Name == idOrName {
- b = br
- ok = true
- break
- }
- }
- }
- if !ok {
- return fmt.Errorf("bridge not found")
- }
- ipCIDR = strings.TrimSpace(ipCIDR)
- var updated []string
- for _, ip := range b.IPAddresses {
- if ip != ipCIDR {
- updated = append(updated, ip)
- }
- }
- b.IPAddresses = updated
- _ = m.save()
- _ = exec.Command("ip", "addr", "del", ipCIDR, "dev", b.Name).Run()
- return nil
- }
- func (m *BridgeManager) syncBridge(b *BridgeInstance) error {
- link, err := netlink.LinkByName(b.Name)
- if err != nil {
- // Create bridge
- args := []string{"link", "add", b.Name, "type", "bridge"}
- if b.VlanFiltering {
- args = append(args, "vlan_filtering", "1")
- } else {
- args = append(args, "vlan_filtering", "0")
- }
- if b.PVID > 0 {
- args = append(args, "vlan_default_pvid", strconv.Itoa(b.PVID))
- }
- if out, err := exec.Command("ip", args...).CombinedOutput(); err != nil {
- return fmt.Errorf("failed to create bridge %s: %s: %w", b.Name, string(out), err)
- }
- link, _ = netlink.LinkByName(b.Name)
- }
- if link != nil {
- // Set MTU
- if b.MTU > 0 {
- _ = netlink.LinkSetMTU(link, b.MTU)
- }
- // Set Admin MAC if specified
- if b.MACAddress != "" {
- if hw, err := net.ParseMAC(b.MACAddress); err == nil {
- _ = netlink.LinkSetHardwareAddr(link, hw)
- }
- }
- // Set VLAN filtering on existing bridge
- vfVal := "0"
- if b.VlanFiltering {
- vfVal = "1"
- }
- pvidVal := strconv.Itoa(b.PVID)
- if b.PVID <= 0 {
- pvidVal = "1"
- }
- _ = exec.Command("ip", "link", "set", b.Name, "type", "bridge", "vlan_filtering", vfVal, "vlan_default_pvid", pvidVal).Run()
- // Set STP
- stpVal := "0"
- if b.STP == "rstp" || b.STP == "stp" {
- stpVal = "1"
- }
- _ = os.WriteFile(fmt.Sprintf("/sys/class/net/%s/bridge/stp_state", b.Name), []byte(stpVal), 0644)
- // Set link up or down
- if b.Disabled {
- _ = netlink.LinkSetDown(link)
- } else {
- _ = netlink.LinkSetUp(link)
- }
- // Sync IP addresses
- for _, ip := range b.IPAddresses {
- _ = exec.Command("ip", "addr", "add", ip, "dev", b.Name).Run()
- }
- }
- return nil
- }
- func (m *BridgeManager) updateBridgeLiveStats(b *BridgeInstance) {
- // Count ports belonging to this bridge
- portsCount := 0
- for _, p := range m.ports {
- if p.Bridge == b.Name && !p.Disabled {
- portsCount++
- }
- }
- b.PortsCount = portsCount
- link, err := netlink.LinkByName(b.Name)
- if err != nil {
- b.Running = false
- return
- }
- attrs := link.Attrs()
- b.Running = (attrs.Flags&net.FlagUp != 0) && (attrs.OperState == netlink.OperUp || attrs.OperState == netlink.OperUnknown)
- if b.MACAddress == "" {
- b.MACAddress = attrs.HardwareAddr.String()
- }
- if attrs.Statistics != nil {
- b.RxPackets = attrs.Statistics.RxPackets
- b.TxPackets = attrs.Statistics.TxPackets
- b.RxBytes = attrs.Statistics.RxBytes
- b.TxBytes = attrs.Statistics.TxBytes
- b.RxHuman = formatBytes(b.RxBytes)
- b.TxHuman = formatBytes(b.TxBytes)
- }
- // Read IP addresses from kernel
- if addrs, err := netlink.AddrList(link, netlink.FAMILY_V4); err == nil {
- var ips []string
- for _, a := range addrs {
- ips = append(ips, a.IPNet.String())
- }
- b.IPAddresses = ips
- }
- }
- // ==================== Bridge Ports ====================
- func (m *BridgeManager) ListPorts() []*BridgePort {
- m.mu.Lock()
- defer m.mu.Unlock()
- list := make([]*BridgePort, 0, len(m.ports))
- for _, p := range m.ports {
- m.updatePortLiveStatus(p)
- list = append(list, p)
- }
- sort.Slice(list, func(i, j int) bool {
- if list[i].Bridge != list[j].Bridge {
- return list[i].Bridge < list[j].Bridge
- }
- return list[i].Interface < list[j].Interface
- })
- return list
- }
- func (m *BridgeManager) SavePort(p BridgePort) error {
- m.mu.Lock()
- defer m.mu.Unlock()
- if p.Bridge == "" || p.Interface == "" {
- return fmt.Errorf("bridge and interface are required")
- }
- if p.PVID <= 0 {
- p.PVID = 1
- }
- if p.Priority <= 0 {
- p.Priority = 128
- }
- if p.PathCost <= 0 {
- p.PathCost = 10
- }
- if p.FrameTypes == "" {
- p.FrameTypes = "admit-all"
- }
- if p.ID == "" {
- p.ID = fmt.Sprintf("port-%d", time.Now().UnixNano())
- }
- m.ports[p.ID] = &p
- _ = m.save()
- return m.syncPort(&p)
- }
- func (m *BridgeManager) DeletePort(id string) error {
- m.mu.Lock()
- defer m.mu.Unlock()
- p, ok := m.ports[id]
- if !ok {
- return fmt.Errorf("port not found")
- }
- // Remove from bridge in kernel
- _ = exec.Command("ip", "link", "set", p.Interface, "nomaster").Run()
- delete(m.ports, id)
- return m.save()
- }
- func (m *BridgeManager) SetPortState(id string, disabled bool) error {
- m.mu.Lock()
- defer m.mu.Unlock()
- p, ok := m.ports[id]
- if !ok {
- return fmt.Errorf("port not found")
- }
- p.Disabled = disabled
- _ = m.save()
- if disabled {
- _ = exec.Command("ip", "link", "set", p.Interface, "nomaster").Run()
- } else {
- _ = m.syncPort(p)
- }
- return nil
- }
- func (m *BridgeManager) syncPort(p *BridgePort) error {
- if p.Disabled {
- _ = exec.Command("ip", "link", "set", p.Interface, "nomaster").Run()
- return nil
- }
- // Check if interface exists
- if _, err := netlink.LinkByName(p.Interface); err != nil {
- return fmt.Errorf("interface %s does not exist", p.Interface)
- }
- // Attach interface to bridge
- if out, err := exec.Command("ip", "link", "set", p.Interface, "master", p.Bridge).CombinedOutput(); err != nil {
- return fmt.Errorf("failed to attach %s to bridge %s: %s: %w", p.Interface, p.Bridge, string(out), err)
- }
- // Bring interface UP
- _ = exec.Command("ip", "link", "set", p.Interface, "up").Run()
- // Port isolation / Split Horizon
- isolated := "off"
- if p.Horizon > 0 {
- isolated = "on"
- }
- _ = exec.Command("bridge", "link", "set", "dev", p.Interface, "isolated", isolated).Run()
- // Learning
- learning := "on"
- if !p.Learn {
- learning = "off"
- }
- _ = exec.Command("bridge", "link", "set", "dev", p.Interface, "learning", learning).Run()
- // Ingress filtering
- ingress := "off"
- if p.IngressFiltering {
- ingress = "on"
- }
- _ = exec.Command("bridge", "link", "set", "dev", p.Interface, "ingress_filtering", ingress).Run()
- // Priority & Path Cost
- prioCostArgs := []string{"link", "set", "dev", p.Interface}
- if p.Priority > 0 {
- prioCostArgs = append(prioCostArgs, "priority", strconv.Itoa(p.Priority))
- }
- if p.PathCost > 0 {
- prioCostArgs = append(prioCostArgs, "cost", strconv.Itoa(p.PathCost))
- }
- if len(prioCostArgs) > 4 {
- _ = exec.Command("bridge", prioCostArgs...).Run()
- }
- // VLAN filtering configuration
- if p.PVID > 0 {
- _ = exec.Command("bridge", "vlan", "add", "dev", p.Interface, "vid", strconv.Itoa(p.PVID), "pvid", "untagged").Run()
- }
- return nil
- }
- func (m *BridgeManager) updatePortLiveStatus(p *BridgePort) {
- if p.Disabled {
- p.Status = "disabled"
- return
- }
- link, err := netlink.LinkByName(p.Interface)
- if err != nil {
- p.Status = "inactive"
- return
- }
- attrs := link.Attrs()
- if attrs.OperState == netlink.OperDown {
- p.Status = "disabled"
- } else if attrs.MasterIndex > 0 {
- p.Status = "forwarding"
- } else {
- p.Status = "unattached"
- }
- }
- // ==================== Bridge VLANs ====================
- func (m *BridgeManager) ListVLANs() []*BridgeVLAN {
- m.mu.Lock()
- defer m.mu.Unlock()
- list := make([]*BridgeVLAN, 0, len(m.vlans))
- for _, v := range m.vlans {
- v.CurrentTagged = v.Tagged
- v.CurrentUntagged = v.Untagged
- list = append(list, v)
- }
- sort.Slice(list, func(i, j int) bool {
- if list[i].Bridge != list[j].Bridge {
- return list[i].Bridge < list[j].Bridge
- }
- return list[i].VlanIDs < list[j].VlanIDs
- })
- return list
- }
- func (m *BridgeManager) SaveVLAN(v BridgeVLAN) error {
- m.mu.Lock()
- defer m.mu.Unlock()
- if v.Bridge == "" || v.VlanIDs == "" {
- return fmt.Errorf("bridge and vlan_ids are required")
- }
- if v.ID == "" {
- v.ID = fmt.Sprintf("bvlan-%d", time.Now().UnixNano())
- }
- m.vlans[v.ID] = &v
- _ = m.save()
- return m.syncVLAN(&v)
- }
- func (m *BridgeManager) DeleteVLAN(id string) error {
- m.mu.Lock()
- defer m.mu.Unlock()
- v, ok := m.vlans[id]
- if !ok {
- return fmt.Errorf("vlan entry not found")
- }
- // Remove VLANs from kernel
- vids := parseVLANRange(v.VlanIDs)
- for _, vid := range vids {
- vidStr := strconv.Itoa(vid)
- for _, iface := range append(v.Tagged, v.Untagged...) {
- _ = exec.Command("bridge", "vlan", "del", "dev", iface, "vid", vidStr).Run()
- }
- }
- delete(m.vlans, id)
- return m.save()
- }
- func (m *BridgeManager) SetVLANState(id string, disabled bool) error {
- m.mu.Lock()
- defer m.mu.Unlock()
- v, ok := m.vlans[id]
- if !ok {
- return fmt.Errorf("vlan entry not found")
- }
- v.Disabled = disabled
- _ = m.save()
- if disabled {
- vids := parseVLANRange(v.VlanIDs)
- for _, vid := range vids {
- vidStr := strconv.Itoa(vid)
- for _, iface := range append(v.Tagged, v.Untagged...) {
- _ = exec.Command("bridge", "vlan", "del", "dev", iface, "vid", vidStr).Run()
- }
- }
- } else {
- _ = m.syncVLAN(v)
- }
- return nil
- }
- func (m *BridgeManager) syncVLAN(v *BridgeVLAN) error {
- if v.Disabled {
- return nil
- }
- vids := parseVLANRange(v.VlanIDs)
- for _, vid := range vids {
- vidStr := strconv.Itoa(vid)
- for _, iface := range v.Tagged {
- _ = exec.Command("bridge", "vlan", "add", "dev", iface, "vid", vidStr).Run()
- }
- for _, iface := range v.Untagged {
- _ = exec.Command("bridge", "vlan", "add", "dev", iface, "vid", vidStr, "untagged").Run()
- }
- }
- return nil
- }
- // ==================== Bridge Hosts (FDB / MAC Table) ====================
- func (m *BridgeManager) ListHosts(bridgeFilter string) ([]BridgeHost, error) {
- args := []string{"fdb", "show"}
- if bridgeFilter != "" {
- args = append(args, "br", bridgeFilter)
- }
- out, err := exec.Command("bridge", args...).Output()
- if err != nil {
- return nil, fmt.Errorf("failed to run bridge fdb show: %w", err)
- }
- var hosts []BridgeHost
- scanner := bufio.NewScanner(strings.NewReader(string(out)))
- for scanner.Scan() {
- line := strings.TrimSpace(scanner.Text())
- if line == "" {
- continue
- }
- fields := strings.Fields(line)
- if len(fields) < 3 {
- continue
- }
- mac := fields[0]
- dev := ""
- vlan := 1
- master := ""
- age := 0.0
- isLocal := false
- isStatic := false
- for i := 1; i < len(fields); i++ {
- switch fields[i] {
- case "dev":
- if i+1 < len(fields) {
- dev = fields[i+1]
- i++
- }
- case "vlan":
- if i+1 < len(fields) {
- vlan, _ = strconv.Atoi(fields[i+1])
- i++
- }
- case "master":
- if i+1 < len(fields) {
- master = fields[i+1]
- i++
- }
- case "self":
- isLocal = true
- case "permanent":
- isLocal = true
- isStatic = true
- case "static":
- isStatic = true
- default:
- if val, err := strconv.ParseFloat(fields[i], 64); err == nil {
- age = val
- }
- }
- }
- if master == "" && strings.HasPrefix(dev, "br") {
- master = dev
- }
- var flags []string
- if isLocal {
- flags = append(flags, "L")
- } else if isStatic {
- flags = append(flags, "S")
- } else {
- flags = append(flags, "D")
- }
- hosts = append(hosts, BridgeHost{
- MAC: mac,
- Interface: dev,
- Bridge: master,
- VlanID: vlan,
- Age: age,
- Local: isLocal,
- Static: isStatic,
- Flags: strings.Join(flags, ","),
- })
- }
- return hosts, nil
- }
- func (m *BridgeManager) FlushHosts(bridge string) error {
- if bridge == "" {
- return fmt.Errorf("bridge name required for flush")
- }
- _ = exec.Command("bridge", "fdb", "flush", "dev", bridge, "dynamic").Run()
- return nil
- }
- // ==================== Summary & SyncAll ====================
- func (m *BridgeManager) GetSummary() BridgeSummary {
- m.mu.RLock()
- bCount := len(m.bridges)
- pCount := len(m.ports)
- vCount := len(m.vlans)
- m.mu.RUnlock()
- hCount := 0
- if hosts, err := m.ListHosts(""); err == nil {
- hCount = len(hosts)
- }
- return BridgeSummary{
- BridgesCount: bCount,
- PortsCount: pCount,
- VLANsCount: vCount,
- HostsCount: hCount,
- }
- }
- func (m *BridgeManager) SyncAll() {
- m.mu.RLock()
- defer m.mu.RUnlock()
- // 1. Sync all bridges
- for _, b := range m.bridges {
- _ = m.syncBridge(b)
- }
- // 2. Sync all ports
- for _, p := range m.ports {
- _ = m.syncPort(p)
- }
- // 3. Sync all VLAN entries
- for _, v := range m.vlans {
- _ = m.syncVLAN(v)
- }
- }
- // RenameInterface updates bridge, port, and VLAN configurations when an interface is renamed.
- func (m *BridgeManager) RenameInterface(oldName, newName string) {
- m.mu.Lock()
- defer m.mu.Unlock()
- changed := false
- for _, b := range m.bridges {
- if b.Name == oldName {
- b.Name = newName
- changed = true
- }
- }
- for _, p := range m.ports {
- if p.Interface == oldName {
- p.Interface = newName
- changed = true
- }
- }
- for _, v := range m.vlans {
- if v.Bridge == oldName {
- v.Bridge = newName
- changed = true
- }
- for i, tagged := range v.Tagged {
- if tagged == oldName {
- v.Tagged[i] = newName
- changed = true
- }
- }
- for i, untagged := range v.Untagged {
- if untagged == oldName {
- v.Untagged[i] = newName
- changed = true
- }
- }
- }
- if changed {
- _ = m.save()
- }
- }
- // Helper: parse "10" or "10,20,30" or "10-20" into int slice
- func parseVLANRange(s string) []int {
- var result []int
- parts := strings.Split(s, ",")
- for _, p := range parts {
- p = strings.TrimSpace(p)
- if strings.Contains(p, "-") {
- rangeParts := strings.Split(p, "-")
- if len(rangeParts) == 2 {
- start, err1 := strconv.Atoi(strings.TrimSpace(rangeParts[0]))
- end, err2 := strconv.Atoi(strings.TrimSpace(rangeParts[1]))
- if err1 == nil && err2 == nil && start <= end {
- for i := start; i <= end; i++ {
- result = append(result, i)
- }
- continue
- }
- }
- }
- if val, err := strconv.Atoi(p); err == nil {
- result = append(result, val)
- }
- }
- return result
- }
- func formatBytes(b uint64) string {
- const unit = 1024
- if b < unit {
- return fmt.Sprintf("%d B", b)
- }
- div, exp := uint64(unit), 0
- for n := b / unit; n >= unit; n /= unit {
- div *= unit
- exp++
- }
- return fmt.Sprintf("%.1f %cB", float64(b)/float64(div), "KMGTPE"[exp])
- }
|