Removed the Licensing section from the contributing guide.
24 KiB
Contributing to NetVisor
Thank you for your interest in contributing to NetVisor! We welcome contributions of all kinds, from bug reports and documentation improvements to new features and service definitions.
Table of Contents
- Getting Started
- Ways to Contribute
- Development Environment Setup
- Development Workflow
- Adding Service Definitions
- Testing
- Submitting Your Contribution
- Licensing
Getting Started
Quick Start for Service Definitions
The easiest way to contribute is by adding service definitions! Service definitions help NetVisor identify and categorize network services during discovery. This is a great first contribution that doesn't require deep knowledge of the codebase. Given the wide variety of services that folks run across their networks, this is inherently best handled as a community-driven effort.
If you're interested in adding a service definition, jump to the Adding Service Definitions section.
Ways to Contribute
1. Service Definitions (Recommended for First-Time Contributors)
Service definitions are small, focused additions that help NetVisor discover and identify specific services on your network. Examples include:
- Home automation platforms (Home Assistant, OpenHAB)
- Media servers (Plex, Jellyfin, Emby)
- Infrastructure services (Pi-hole, AdGuard, Traefik)
- Development tools (Portainer, Grafana, Jenkins)
2. Bug Reports
Found a bug? Please open an issue!
3. Documentation
Help improve our documentation:
- Fix typos or clarify existing docs
- Add examples or tutorials for specific setups
- Improve installation instructions
- Document troubleshooting steps
4. Code Contributions
For larger features or bug fixes:
- Discuss your idea in an issue first
- Follow the development workflow below
- Write tests for new functionality
- Update documentation as needed
Development Environment Setup
Prerequisites
For Daemon Development:
- Linux: Docker with host networking support, OR binary installation
- Mac/Windows: Binary installation only (Docker Desktop does not support host networking)
For Server Development:
- Rust 1.90 or later
- Node.js 20 or later
- PostgreSQL 17
- Docker and Docker Compose (optional, for containerized development)
Initial Setup
-
Clone the repository
git clone https://github.com/mayanayza/netvisor.git cd netvisor -
Install development dependencies
On Ubuntu/Debian:
-
Install NVM and Node.js 20
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.40.0/install.sh | bash nvm install 20 nvm use 20 -
Install postgresql-17
sudo apt install curl ca-certificates gnupg2 wget vim -y sudo install -d /usr/share/postgresql-common/pgdg sudo curl -o /usr/share/postgresql-common/pgdg/apt.postgresql.org.asc --fail https://www.postgresql.org/media/keys/ACCC4CF8.asc sudo sh -c 'echo "deb [arch=amd64 signed-by=/usr/share/postgresql-common/pgdg/apt.postgresql.org.asc] https://apt.postgresql.org/pub/repos/apt $(lsb_release -cs)-pgdg main" > /etc/apt/sources.list.d/pgdg.list' sudo apt update sudo apt -y install postgresql-17 -
Install project dependencies
make install-dev-linux
On MacOS:
-
Install Homebrew if not already installed
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" -
Install Rust, Node.js 20, and PostgreSQL 17
brew install rust node@20 postgresql@17 -
Install project dependencies
make install-dev-macThis installs:
- Rust toolchain with rustfmt and clippy
- Node.js dependencies
-
-
Set up the database
make setup-dbThis starts a PostgreSQL container on port 5432.
Development Environments
You have two options for development:
Option 1: Local Development (Recommended)
Run components individually with hot reload:
# Terminal 1 - Start the server
make dev-server
# Terminal 2 - Start the UI
make dev-ui
# Terminal 3 - Start the daemon (if needed)
make dev-daemon
Advantages:
- Faster iteration with hot reload
- Easier debugging
- More control over individual components
Option 2: Containerized Development
Run everything in Docker containers:
# Start all services
make dev-container
# Rebuild containers
make dev-container-rebuild
# Clean rebuild (no cache)
make dev-container-rebuild-clean
# Stop all services
make dev-down
Use this when:
- Testing the full stack together
- You want a production-like environment
- You're having dependency issues locally
Accessing the Application
Once running:
- UI: http://localhost:5173 (with hot reload)
- Server API: http://localhost:60072
- Daemon API: http://localhost:60073
Development Workflow
Before You Start
-
Create a new branch for your work:
git checkout -b feature/your-feature-name # or git checkout -b fix/your-bug-fix -
If working on the server/daemon, ensure fresh start:
make clean-daemon # Clear daemon config make clean-db # Stop and remove database make setup-db # Create fresh database
During Development
-
Write your code
- Follow existing code patterns
- Add comments for complex logic
- Keep changes focused and atomic
-
Test your changes
make testNote - this will tear down all containers, including the PostgreSql container; you'll need to recreate that after running.
You can dump the DB if you want to hold on to the data and reload the container from the dump.
make dump-db -
Format your code
make format -
Lint your code
make lint
Before Submitting
Always run these commands before creating a PR:
make format # Format all code
make lint # Check for issues
make test # Run all tests
All three commands must pass without errors before submitting your PR.
Adding Service Definitions
Service definitions are the best place to start contributing! They help NetVisor identify and categorize services during network discovery.
Project Structure
Service definitions are located in:
backend/src/server/services/definitions/
├── mod.rs # Module registry
├── home_assistant.rs # Example service definition
├── plex.rs # Example service definition
└── your_service.rs # Your new service definition
Step 1: Create Your Service File
Create a new file in backend/src/server/services/definitions/ named after your service (e.g., grafana.rs):
use crate::server::hosts::types::ports::PortBase;
use crate::server::services::definitions::{create_service, ServiceDefinitionFactory};
use crate::server::services::types::categories::ServiceCategory;
use crate::server::services::types::definitions::ServiceDefinition;
use crate::server::services::types::patterns::Pattern;
#[derive(Default, Clone, Eq, PartialEq, Hash)]
pub struct Grafana;
impl ServiceDefinition for Grafana {
fn name(&self) -> &'static str {
"Grafana"
}
fn description(&self) -> &'static str {
"Metrics dashboard and visualization platform"
}
fn category(&self) -> ServiceCategory {
ServiceCategory::Monitoring
}
fn discovery_pattern(&self) -> Pattern<'_> {
Pattern::Endpoint(
PortBase::Http,
"/api/health",
"grafana"
)
}
fn logo_url(&self) -> &'static str {
"https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/service-logo.svg"
}
}
// This macro registers your service for automatic discovery
inventory::submit!(ServiceDefinitionFactory::new(create_service::<Grafana>));
Step 2: Register the Module
Add your module to backend/src/server/services/definitions/mod.rs:
pub mod grafana; // Add this line
That's it! Your service will now be automatically discovered during network scans.
Understanding Pattern Types
Patterns define how NetVisor identifies your service.
Here are the available pattern types:
Endpoint Patterns
This is the preferred match type, as the existence of the name of the service in a response is a strong signal that it is in fact the service in question.
That said, some services will contain the unique name of a service in circumstances like:
- Dashboards will contain multiple service names depending on the service being displayed
- Service names that are short or parts of common words can be contained in other words (ie "Plex" is part of the word "Complex", so if a service has the word "Complex" on the endpoint being checked it will cause a false positive)
So, it's best to include another pattern alongside a Pattern::Endpoint just to be sure, or use a very specific string match (ie a phrase rather than a word).
Pattern::Endpoint Check if an endpoint returns expected content:
fn discovery_pattern(&self) -> Pattern<'_> {
Pattern::Endpoint(
PortBase::Http, // Port to check
"/api/service", // Path
"service_name" // Expected text in response
)
}
Simple Port Patterns
This pattern is acceptable if there are no usable endpoints (ie they require authentication, SSL, or otherwise don't provide service-identifying information), but try to create a pattern with multiple unique ports or combine ports with other information to make the match more precise.
Pattern::Port Match a specific port:
fn discovery_pattern(&self) -> Pattern<'_> {
Pattern::Port(PortBase::Http) // Port 80
}
Common PortBase values:
PortBase::Http(80)PortBase::Https(443)PortBase::HttpAlt(8080)PortBase::Ssh(22)PortBase::DnsUdp(53)- For custom ports:
PortBase::new_tcp(8000)orPortBase::new_udp(1900)
Note UDP pattern matching is barely supported outside of DNS and a few others. Please don't rely heavily on UDP ports.
Logical Patterns
Pattern::AnyOf Match if ANY pattern succeeds:
fn discovery_pattern(&self) -> Pattern<'_> {
Pattern::AnyOf(vec![
Pattern::Port(PortBase::new_tcp(32400)),
Pattern::Endpoint(PortBase::Http, "/web", "Plex", None)
])
}
Pattern::AllOf Match ONLY if ALL patterns succeed:
fn discovery_pattern(&self) -> Pattern<'_> {
Pattern::AllOf(vec![
Pattern::Port(PortBase::Http),
Pattern::Port(PortBase::new_tcp(8443))
])
}
Pattern::Not Inverse of a pattern:
fn discovery_pattern(&self) -> Pattern<'_> {
Pattern::Not(&Pattern::IsGateway)
}
Special Patterns
Pattern::IsGateway Matches if the host is in the routing table as a gateway:
fn discovery_pattern(&self) -> Pattern<'_> {
Pattern::IsGateway
}
Pattern::MacVendor Match based on MAC address vendor:
use crate::server::services::types::patterns::Vendor;
fn discovery_pattern(&self) -> Pattern<'_> {
Pattern::MacVendor(Vendor::EERO)
}
To add new Vendor:: values:
- Go to
backend/src/server/services/types/patterns.rsand ctrl+f "pub struct Vendor;" - Use
https://gist.github.com/aallan/b4bb86db86079509e6159810ae9bd3e4to identify the string used by a vendor for their MAC address patterns. - Add your new Vendor value:
pub const NEWVENDOR: &'static str = "Acme, Inc"
```;
**Pattern::SubnetIsType**
Match based on subnet type:
```rust
use crate::server::subnets::types::base::SubnetType;
fn discovery_pattern(&self) -> Pattern<'_> {
Pattern::SubnetIsType(SubnetType::Guest)
}
For a list of subnet types and information on how they are derived, check out backend/src/server/subnets/types/base.rs.
pub enum SubnetType has the list, and the method from_interface_name has specifics on how they are matched.
Pattern::None For services that aren't auto-discovered (manual only):
fn discovery_pattern(&self) -> Pattern<'_> {
Pattern::None
}
Service Categories
Choose the most appropriate category. If the service you want to add doesn't fit the category, you can add one at backend/src/server/services/types/categories.rs.
Infrastructure
ServiceCategory::NetworkCore- Switches, core infrastructureServiceCategory::NetworkAccess- Routers, access pointsServiceCategory::NetworkSecurity- Firewalls, security appliancesServiceCategory::DNS- DNS serversServiceCategory::VPN- VPN serversServiceCategory::ReverseProxy- Nginx, Traefik, HAProxy, etc
Server Services
ServiceCategory::Storage- NAS, file serversServiceCategory::Media- Plex, Jellyfin, EmbyServiceCategory::HomeAutomation- Home Assistant, OpenHABServiceCategory::Virtualization- Proxmox, VMware, DockerServiceCategory::Backup- Backup services
Applications
ServiceCategory::Web- Web servers and applicationsServiceCategory::Database- Database serversServiceCategory::Development- Development toolsServiceCategory::Dashboard- Dashboards, admin panelsServiceCategory::Monitoring- Monitoring and metrics
Devices
ServiceCategory::Workstation- Desktop computersServiceCategory::Mobile- Mobile devicesServiceCategory::IoT- IoT devicesServiceCategory::Printer- Printers
Other
ServiceCategory::AdBlock- Pi-hole, AdGuardServiceCategory::Custom- Custom servicesServiceCategory::Unknown- When unclear
Optional Properties
Generic Services
Mark services not tied to a specific brand.
fn is_generic(&self) -> bool {
true
}
Service Icons
NetVisor supports icons from three sources.
Dashboard Icons (Recommended - has the most service icons):
https://dashboardicons.com/icons/home-assistant
Search for the service and press the link button to get a URL like
"https://cdn.jsdelivr.net/gh/homarr-labs/dashboard-icons/svg/home-assistant.svg"
Simple Icons:
simpleicons.org/icons/
Search for the service and right click an image to open in a new tab and get the URL like:
https://simpleicons.org/icons/homeassistant.svg
Vector Logo Zone:
vectorlogo.zone/logos/
Search for the service then press the clipboard button to get a URL like:
https://www.vectorlogo.zone/logos/akamai/akamai-icon.svg
White Background (for dark logos):
fn logo_needs_white_background(&self) -> bool {
true
}
Browse available icons:
- Dashboard Icons: https://dashboardicons.com/
- Simple Icons: https://simpleicons.org/icons/
- Vector Logo Zone: https://www.vectorlogo.zone/
Complete Examples
Simple Port-Based Service
use crate::server::hosts::types::ports::PortBase;
use crate::server::services::definitions::{create_service, ServiceDefinitionFactory};
use crate::server::services::types::categories::ServiceCategory;
use crate::server::services::types::definitions::ServiceDefinition;
use crate::server::services::types::patterns::Pattern;
#[derive(Default, Clone, Eq, PartialEq, Hash)]
pub struct Redis;
impl ServiceDefinition for Redis {
fn name(&self) -> &'static str { "Redis" }
fn description(&self) -> &'static str { "In-memory data structure store" }
fn category(&self) -> ServiceCategory { ServiceCategory::Database }
fn discovery_pattern(&self) -> Pattern<'_> {
Pattern::Port(PortBase::new_tcp(6379))
}
fn simple_icons_path(&self) -> &'static str { "redis" }
}
inventory::submit!(ServiceDefinitionFactory::new(create_service::<Redis>));
Web Service with Health Check
use crate::server::hosts::types::ports::PortBase;
use crate::server::services::definitions::{create_service, ServiceDefinitionFactory};
use crate::server::services::types::categories::ServiceCategory;
use crate::server::services::types::definitions::ServiceDefinition;
use crate::server::services::types::patterns::Pattern;
#[derive(Default, Clone, Eq, PartialEq, Hash)]
pub struct Portainer;
impl ServiceDefinition for Portainer {
fn name(&self) -> &'static str { "Portainer" }
fn description(&self) -> &'static str { "Docker container management interface" }
fn category(&self) -> ServiceCategory { ServiceCategory::Virtualization }
fn discovery_pattern(&self) -> Pattern<'_> {
Pattern::Endpoint(
PortBase::HttpAlt,
"/api/status",
"Portainer"
)
}
fn dashboard_icons_path(&self) -> &'static str { "portainer" }
}
inventory::submit!(ServiceDefinitionFactory::new(create_service::<Portainer>));
Complex Multi-Pattern Service
use crate::server::hosts::types::ports::PortBase;
use crate::server::services::definitions::{create_service, ServiceDefinitionFactory};
use crate::server::services::types::categories::ServiceCategory;
use crate::server::services::types::definitions::ServiceDefinition;
use crate::server::services::types::patterns::Pattern;
#[derive(Default, Clone, Eq, PartialEq, Hash)]
pub struct HomeAssistant;
impl ServiceDefinition for HomeAssistant {
fn name(&self) -> &'static str { "Home Assistant" }
fn description(&self) -> &'static str { "Open-source home automation platform" }
fn category(&self) -> ServiceCategory { ServiceCategory::HomeAutomation }
fn discovery_pattern(&self) -> Pattern<'_> {
Pattern::AnyOf(vec![
// Check API endpoint
Pattern::Endpoint(
PortBase::HttpAlt,
"/api/",
"Home Assistant"
),
// Or check default port with web response
Pattern::AllOf(vec![
Pattern::Port(PortBase::new_tcp(8123)),
Pattern::Endpoint(PortBase::Http, "/", "homeassistant", None)
])
])
}
fn dashboard_icons_path(&self) -> &'static str { "home-assistant" }
}
inventory::submit!(ServiceDefinitionFactory::new(create_service::<HomeAssistant>));
Gateway Service with MAC Vendor
use crate::server::services::definitions::{create_service, ServiceDefinitionFactory};
use crate::server::services::types::categories::ServiceCategory;
use crate::server::services::types::definitions::ServiceDefinition;
use crate::server::services::types::patterns::{Pattern, Vendor};
#[derive(Default, Clone, Eq, PartialEq, Hash)]
pub struct EeroGateway;
impl ServiceDefinition for EeroGateway {
fn name(&self) -> &'static str { "Eero Gateway" }
fn description(&self) -> &'static str { "Eero mesh WiFi router" }
fn category(&self) -> ServiceCategory { ServiceCategory::NetworkAccess }
fn discovery_pattern(&self) -> Pattern<'_> {
Pattern::AllOf(vec![
Pattern::MacVendor(Vendor::EERO),
Pattern::IsGateway
])
}
fn vector_logo_zone_icons_path(&self) -> &'static str { "eero/eero-icon" }
fn logo_needs_white_background(&self) -> bool { true }
}
inventory::submit!(ServiceDefinitionFactory::new(create_service::<EeroGateway>));
Testing
Running Tests
Before submitting any PR, you must run all tests:
make test
This will:
- Stop any running dev containers
- Clean daemon config
- Run all backend and integration tests
Testing Your Service Definition
1. Verify Compilation
make dev-server
Check the server logs for any compilation errors.
2. Test Discovery
If you have the actual service running on your network:
- Start NetVisor with your changes
- Navigate to the discovery page in the UI
- Run a network scan
- Verify your service is detected and correctly categorized
- Check that the icon displays correctly
3. Manual Testing
Even if you don't have the service running, you should verify:
- The service compiles without errors
- The pattern logic makes sense
- The category is appropriate
- The icon loads correctly
Writing Tests (Optional but Appreciated)
If you're adding complex logic, consider adding unit tests:
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_service_properties() {
let service = YourService;
assert_eq!(service.name(), "Your Service");
assert_eq!(service.category(), ServiceCategory::Web);
}
}
Submitting Your Contribution
Before You Submit
Pre-commit Hooks
NetVisor uses pre-commit hooks to ensure code quality. These hooks run automatically:
- On commit: Format and lint checks
- On push: Full test suite
The hooks are installed automatically when you run make install-dev-mac or make install-dev-linux.
To skip hooks when needed (not recommended):
git commit --no-verify # Skip commit hooks
git push --no-verify # Skip push hooks
Pre-submission checklist:
- Created a descriptive branch name
- Code follows existing patterns and conventions
- Ran
make formatto format all code - Ran
make lintwith no errors - Ran
make testwith all tests passing - Tested your changes (if possible)
- Updated documentation (if needed)
- Committed with clear, descriptive messages
Pull Request Guidelines
-
One change per PR: Keep PRs focused
- One service definition per PR
- One bug fix per PR
- Related changes can be grouped
-
Clear title: Use descriptive titles
Add service definition for GrafanaFix port scanning timeout issueUpdate installation documentation
-
Good description: Include context and details
- What problem does this solve?
- How did you test it?
- Any breaking changes?
- Screenshots (for UI changes)
PR Template for Service Definitions
## Add service definition for [Service Name]
**Description**: [Brief description of what this service does]
**Official Website**: [URL]
**Default Ports**: [List the ports this service typically uses]
**Discovery Method**: [Explain the pattern used and why]
- Pattern type: [Port/Endpoint/Other]
- Reasoning: [Why this pattern is appropriate]
**Icon Source**: [Dashboard Icons/Simple Icons/Vector Logo Zone]
**Testing**:
- [ ] Compiles successfully
- [ ] Tested against real instance (describe setup below)
- [ ] Unable to test (explain why below)
**Testing Details**:
[Describe how you tested this, or why you couldn't test it]
**Additional Notes**:
[Any special considerations, edge cases, or future improvements]
After Submitting
- Make requested changes in new commits (don't force-push)
- Be open to feedback and suggestions
Getting Help
- Questions? Open a discussion on GitHub
- Stuck? Comment on your PR or issue
Code of Conduct
- Be respectful and professional
- Provide constructive feedback
- Help others learn and grow
- Follow the project's coding standards
Contributor License Agreement
By submitting a contribution to NetVisor, you agree to the following terms:
-
You grant the NetVisor project maintainers a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable copyright license to reproduce, prepare derivative works of, publicly display, publicly perform, sublicense, and distribute your contributions and such derivative works under any license (including commercial licenses).
-
You grant the NetVisor project maintainers a perpetual, worldwide, non-exclusive, no-charge, royalty-free, irrevocable patent license to make, have made, use, offer to sell, sell, import, and otherwise transfer your contributions.
-
You represent that you are legally entitled to grant the above licenses. If your employer has rights to intellectual property that you create, you represent that you have received permission to make the contributions on behalf of that employer, or that your employer has waived such rights for your contributions.
-
You represent that your contribution is your original creation and that you have not copied it from another source.
-
Your contributions will also be licensed to the public under the GNU Affero General Public License v3.0 (AGPL-3.0).
Thank you for contributing to NetVisor! Every contribution, no matter how small, helps make network discovery and documentation better for everyone.