Skip to content

Agent for SNMP Devices#1795

Open
marmar76 wants to merge 3 commits intohenrygd:mainfrom
marmar76:mikrotik-integration
Open

Agent for SNMP Devices#1795
marmar76 wants to merge 3 commits intohenrygd:mainfrom
marmar76:mikrotik-integration

Conversation

@marmar76
Copy link
Copy Markdown
Contributor

@marmar76 marmar76 commented Mar 5, 2026

📃 Description

This PR adds support for monitoring MikroTik devices via SNMP from the internal agent.

If the environment variable BESZEL_AGENT_MIKROTIK_IP is set, the agent will query SNMP statistics from the specified MikroTik device instead of relying solely on local network metrics.
I tested it on hAP AX 2

The goal is to allow Beszel agents running inside a MikroTik container to collect accurate network interface statistics from the host router.

📖 Documentation

Documentation will be added after the implementation is reviewed and the approach is confirmed.

🪵 Changelog

➕ Added

  • SNMP package for querying MikroTik interface statistics
  • Support for BESZEL_AGENT_MIKROTIK_IP environment variable

📷 Screenshots

image image

cummulative download is accurate !!
image

💬 Context

This PR follows the discussion in issue #1537.

🙋 Notes

This is my first time working with Go, so feedback is very welcome.
The current implementation works for my setup, but I'm sure there are better or more idiomatic approaches.
Also, the agent size is getting bigger around 500kb
image

Any suggestions or improvements are appreciated!

@marmar76 marmar76 requested a review from henrygd as a code owner March 5, 2026 16:48
@marmar76
Copy link
Copy Markdown
Contributor Author

marmar76 commented Mar 6, 2026

Im also opening my beszel instance at https://beszel.kusuri.my.id/
email: guest@kusuri.my.id
pass: NothingToSee
you can see the implementation yourself

@henrygd
Copy link
Copy Markdown
Owner

henrygd commented Mar 6, 2026

Thanks so much for your work on this. I'm swamped right now but I'll get back to you as soon as I can.

@henrygd henrygd moved this to Next in Beszel Roadmap Mar 6, 2026
Copy link
Copy Markdown

@bassiebal bassiebal left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @marmar76,

As I was searching for this exact functionality I thought I'd read up on your PR and to speed things along here are some comments you can already have a look at pending the review of @henrygd:

Looking at it, I would argue this is a generic SNMP provider, not a MikroTik specific integration.

All OIDs used here are standard IF-MIB (RFC 2863). They work on any SNMP-capable device: Cisco, UniFi, Synology, etc. The file should be renamed from mikrotik_helpers.go to something like snmp_network.go, and all Mikrotik prefixed functions and env vars should follow. MIKROTIK_IP becomes SNMP_TARGET, etc.

Like I mention in my comment as well, the existing codebase uses manager structs (dockerManager, gpuManager etc) for optional services. SNMP arguably fits that same pattern.

A NetworkIOProvider interface with a local /proc implementation as default and an SNMP implementation selected at startup when SNMP_TARGET is set would keep network.go clean. The SNMP client gets initialized once, stored on the provider struct, and reused for every poll. This also avoids the current issue of reinitializing gosnmp.Default globals in multiple places.

Finally the SNMP version is hardcoded to v2c and the community string to "public". At minimum these should be env vars (SNMP_TARGET, SNMP_COMMUNITY, SNMP_VERSION). v3 support can come later but locking to v2c rules out anyone using v3.

Apologies for the wall of comments on someone else's repo. I just want to see this feature land and hopefully this feedback helps get it there faster.

Great start overall, especially for a first Go project. The collection logic works and your demo instance looks solid!

PS: an example of the interface approach I mentioned:

 // NetworkIOProvider abstracts network interface stats collection.
  type NetworkIOProvider interface {
      IOCounters() ([]psutilNet.IOCountersStat, error)
  }

  // localNetworkIO reads from /proc via gopsutil (default).
  type localNetworkIO struct{}

  func (l *localNetworkIO) IOCounters() ([]psutilNet.IOCountersStat, error) {
      return psutilNet.IOCounters(true)
  }

  // snmpNetworkIO reads interface stats from a remote SNMP target.
  type snmpNetworkIO struct {
      client *gosnmp.GoSNMP
  }

  func newSNMPNetworkIO(target, community string) (*snmpNetworkIO, error) {
      client := &gosnmp.GoSNMP{
          Target:    target,
          Community: community,
          Version:   gosnmp.Version2c,
          Timeout:   2 * time.Second,
      }
      if err := client.Connect(); err != nil {
          return nil, fmt.Errorf("snmp connect: %w", err)
      }
      return &snmpNetworkIO{client: client}, nil
  }

  func (s *snmpNetworkIO) IOCounters() ([]psutilNet.IOCountersStat, error) {
      // walk IF-MIB, return []psutilNet.IOCountersStat
  }

Then in NewAgent():

  if target, exists := utils.GetEnv("SNMP_TARGET"); exists {
      community, _ := utils.GetEnv("SNMP_COMMUNITY")
      if community == "" {
          community = "public"
      }
      agent.networkIO, err = newSNMPNetworkIO(target, community)
  } else {
      agent.networkIO = &localNetworkIO{}
  }

Comment thread agent/mikrotik_helpers.go Outdated

func getMikrotikInterfaces() []int {
Interfaces := []int{}
gosnmp.Default.Target = "192.168.10.2" // RouterOS IP
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This hardcoded IP should be removed and replaced with the IP read from the environment variable.

Comment thread agent/mikrotik_helpers.go Outdated
func getMikrotikInterfaces() []int {
Interfaces := []int{}
gosnmp.Default.Target = "192.168.10.2" // RouterOS IP
gosnmp.Default.Community = "public"
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This community string should be the default but read from an environment variable since a lot of people change it on their device.

Comment thread agent/snmp.go
func GetMikrotikInterfacesStats() []psutilNet.IOCountersStat {
OIDHelper := MikrotikOID()

// // Get System Name
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These comments can probably be removed.

Comment thread agent/network.go
tracker.Cycle()

for _, v := range netIO {
// fmt.Println("_", v)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This comment should be removed as it is a disabled debugging line.

Comment thread agent/mikrotik_helpers.go Outdated
)

// toBig converts any numeric SNMP value to *big.Int safely.
func toBig(v interface{}) *big.Int {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This reimplements what gosnmp.ToBigInt already provides. For the SNMP counter types used here (Counter32, Counter64, Gauge32), both produce identical results.

Comment thread agent/snmp.go
}
InterfacesResult = append(InterfacesResult, thisInterface)
}
// for _, v := range InterfacesResult {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This commented out code block can probably be removed (see my other comment).

Comment thread agent/mikrotik_helpers.go Outdated
gosnmp.Default.Target = "192.168.10.2" // RouterOS IP
gosnmp.Default.Community = "public"
gosnmp.Default.Version = gosnmp.Version2c
gosnmp.Default.Timeout = time.Duration(2) * time.Second
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In general you are re-initializing the gosnmp package in multiple places, this should be consolidated and ideally the connection should be reused.

Comment thread agent/network.go Outdated
return cfg.isBlacklist
}

func getNetIO() ([]psutilNet.IOCountersStat, error) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This check puts device-specific logic in network.go. The existing codebase uses manager structs (dockerManager, gpuManager etc) for optional services. SNMP arguably fits that same pattern..

My approach would be a NetworkIOProvider interface:

  type NetworkIOProvider interface {
      IOCounters() ([]psutilNet.IOCountersStat, error)
  }

With the current implementation (/proc) as the default, and an SNMP implementation selected at startup when SNMP_TARGET is set.

Comment thread agent/network.go Outdated
// [upload bytes, download bytes, total upload, total download]
// if something here
netIO, err := getNetIO()
if err != nil {
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If im not mistaken this error check is inverted.

Comment thread agent/mikrotik_helpers.go Outdated

err := gosnmp.Default.Connect()
if err != nil {
log.Fatalf("Connect() err: %v", err)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You are using log.Fatalf in multiple places, this will kill the agent which seems excessive. Beszel uses slog.Error everywhere else.

@marmar76
Copy link
Copy Markdown
Contributor Author

marmar76 commented Mar 17, 2026

Hi @bassiebal, thank you for your feedback — I really appreciate it. I completely missed that my IP was hard‑coded. You're right: an SNMP implementation should behave more like a database connection — initialized once and kept alive. My current approach doesn’t reflect that well, so I’ll work on improving it based on your suggestions.

I’ve only been using SNMP on a MikroTik router, so I focused specifically on that. I didn’t realize other devices also commonly expose SNMP, so thanks for pointing that out.

I’d also love your feedback on this discussion: #1537.
That one is about a first‑party approach where MikroTik itself hosts the agent. I’m curious what you think about using a third‑party agent instead, so everything is fully SNMP‑based when collecting data from the target.
Thanks again for taking the time to review my work!

@bassiebal
Copy link
Copy Markdown

No problem! And I gave some feedback as well on the discussion. In my opinion a remote agent polling SNMP seems like the best approach but that entirely depends on the scope of this project. I can also imagine it a good solution to skip networking gear entirely to keep the project lean and simple.

- Add NetworkIOProvider interface to support multiple network IO implementations
- Implement localNetworkIO for standard network stats and snmpNetworkIO for SNMP-based devices
- Replace direct Mikrotik checks with provider pattern initialization
- Change environment variable from MIKROTIK_IP to SNMP_TARGET for broader SNMP device support
- Rename mikrotik_helpers.go to snmp.go and update function names (PrettyValue -> SNMP_PrettyValue)
- Remove toBig() helper in favor of gosnmp.ToBigInt()
- Centralize network IO logic through agent.networkIO provider instead of conditional checks in getNetIO()

This refactoring improves code maintainability by abstracting network IO operations behind an interface, making it easier to add support for additional SNMP devices beyond Mikrotik.
@marmar76
Copy link
Copy Markdown
Contributor Author

Hi @bassiebal,
I’ve updated the code with your suggestion, but the agent size still hasn’t changed.
image

I also just learned that Go has this kind of abstraction—where you can call the same method but get different results depending on how the struct is initialized. It really taught me OOP on a deeper level.
I’ve installed it on my MikroTik, and the demo above is already using the newest image.
Thanks again for your guidance!

@marmar76 marmar76 changed the title Agent for mikrotik Agent for SMTP Devices Mar 29, 2026
@svenvg93 svenvg93 changed the title Agent for SMTP Devices Agent for SNMP Devices Mar 29, 2026
@Eirikr70
Copy link
Copy Markdown

Eirikr70 commented Apr 4, 2026

Hello, is the VulnCheck conflict a barrier to this pull request ?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Next

Development

Successfully merging this pull request may close these issues.

4 participants