A comprehensive Go library for implementing NETCONF client connections supporting both NETCONF 1.0 and 1.1 protocols with automatic version detection and flexible transport layers.
- Protocol Support: NETCONF 1.0 and 1.1 with automatic version detection
- Transport Interface: Extensible transport interface with built-in SSH and TLS implementations
- Standard Operations: Get, GetConfig, EditConfig, CopyConfig, DeleteConfig, Lock, Unlock, Commit, DiscardChanges, Validate, KillSession
- Partial Lock: RFC 5717 compliant partial locking of configuration subtrees
- With-defaults: RFC 6243 compliant control of default values in responses
- Notifications: Subscribe to and handle NETCONF notifications and events
- Connection Pooling: Built-in connection pool for managing multiple concurrent connections
- Concurrency Safe: Thread-safe operations with proper mutex handling
- RFC 6241: Network Configuration Protocol (NETCONF) - Core protocol implementation
- RFC 6242: Using the NETCONF Protocol over Secure Shell (SSH) - SSH transport layer
- RFC 7589: Using the NETCONF Protocol over Transport Layer Security (TLS) - TLS transport layer
- RFC 5717: Partial Lock Remote Procedure Call (RPC) for NETCONF - Partial locking capabilities
- RFC 6243: With-defaults Capability for NETCONF - Default value handling in responses
go get github.com/vitalvas/gonetconfpackage main
import (
"context"
"log"
"time"
"github.com/sirupsen/logrus"
"github.com/vitalvas/gonetconf"
"golang.org/x/crypto/ssh"
)
func main() {
// Configure SSH authentication
sshConfig := &ssh.ClientConfig{
User: "admin",
Auth: []ssh.AuthMethod{
ssh.Password("password"),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(), // Use proper verification in production
Timeout: 30 * time.Second,
}
// Create logger for transport
logger := logrus.New()
logger.SetLevel(logrus.InfoLevel) // Set appropriate log level
// Create SSH transport
transport, err := gonetconf.NewSSHTransport("192.168.1.1:830", sshConfig, 30*time.Second, logger)
if err != nil {
log.Fatal(err)
}
config := &gonetconf.Config{
Address: "192.168.1.1:830", // host:port format
Transport: transport,
Timeout: 30 * time.Second,
}
client := gonetconf.NewClient(config)
ctx := context.Background()
if err := client.Connect(ctx); err != nil {
log.Fatal(err)
}
defer client.Close()
// NETCONF version is automatically detected during handshake
log.Printf("Connected with NETCONF version: %s", client.Session().Version)
// Get running configuration
reply, err := client.GetConfig(ctx, gonetconf.DatastoreRunning, nil)
if err != nil {
log.Fatal(err)
}
log.Printf("Operation successful: %v", reply.OK != nil)
}package main
import (
"context"
"crypto/tls"
"log"
"time"
"github.com/sirupsen/logrus"
"github.com/vitalvas/gonetconf"
)
func main() {
// Configure TLS settings
tlsConfig := &tls.Config{
ServerName: "netconf-server.example.com",
MinVersion: tls.VersionTLS12,
// Add certificates, CA settings, etc. as needed
}
// Create logger for transport
logger := logrus.New()
logger.SetLevel(logrus.InfoLevel) // Set appropriate log level
// Create TLS transport
transport, err := gonetconf.NewTLSTransport("192.168.1.1:6513", tlsConfig, 30*time.Second, logger)
if err != nil {
log.Fatal(err)
}
config := &gonetconf.Config{
Address: "192.168.1.1:6513", // Standard NETCONF over TLS port
Transport: transport,
Timeout: 30 * time.Second,
}
client := gonetconf.NewClient(config)
ctx := context.Background()
if err := client.Connect(ctx); err != nil {
log.Fatal(err)
}
defer client.Close()
// Use client for operations...
}// Load SSH private key
keyData, err := os.ReadFile("/path/to/private/key")
if err != nil {
log.Fatal(err)
}
key, err := ssh.ParsePrivateKey(keyData)
if err != nil {
log.Fatal(err)
}
// Configure SSH with key authentication
sshConfig := &ssh.ClientConfig{
User: "admin",
Auth: []ssh.AuthMethod{
ssh.PublicKeys(key),
ssh.Password("fallback_password"), // Optional fallback
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(), // Use proper verification in production
Timeout: 30 * time.Second,
}
// Create logger
logger := logrus.New()
logger.SetLevel(logrus.InfoLevel)
// Create SSH transport
transport, err := gonetconf.NewSSHTransport("192.168.1.1:830", sshConfig, 30*time.Second, logger)
if err != nil {
log.Fatal(err)
}The Config struct uses the Transport interface directly:
config := &gonetconf.Config{
Address: "hostname:port", // Required: host:port format (e.g., "192.168.1.1:830")
Transport: transport, // Required: Transport implementation (SSH or TLS)
Timeout: 30 * time.Second, // Optional: Connection timeout (default: 30s)
}// SSH transport with password authentication
sshConfig := &ssh.ClientConfig{
User: "admin",
Auth: []ssh.AuthMethod{
ssh.Password("password"),
},
HostKeyCallback: ssh.InsecureIgnoreHostKey(), // Use proper verification in production
Timeout: 30 * time.Second,
}
logger := logrus.New()
logger.SetLevel(logrus.InfoLevel)
transport, err := gonetconf.NewSSHTransport("192.168.1.1:830", sshConfig, 30*time.Second, logger)// TLS transport
tlsConfig := &tls.Config{
ServerName: "netconf-server.example.com",
MinVersion: tls.VersionTLS12,
// Add certificates, CA settings, etc. as needed
}
logger := logrus.New()
logger.SetLevel(logrus.InfoLevel)
transport, err := gonetconf.NewTLSTransport("192.168.1.1:6513", tlsConfig, 30*time.Second, logger)poolConfig := &gonetconf.PoolConfig{
MinSize: 2, // Minimum pool size (default: 1)
MaxSize: 10, // Maximum pool size (default: 10)
IdleTimeout: 5 * time.Minute, // Idle connection timeout (default: 5m)
}
pool, err := gonetconf.NewPool(config, poolConfig)
if err != nil {
log.Fatal(err)
}
defer pool.Close()
ctx := context.Background()
if err := pool.Initialize(ctx); err != nil {
log.Fatal(err)
}
// Get client from pool
client, err := pool.Get(ctx)
if err != nil {
log.Fatal(err)
}
defer pool.Put(client)
// Use client for operations
reply, err := client.GetConfig(ctx, gonetconf.DatastoreRunning, nil)// Get operational data
reply, err := client.Get(ctx, filter)
// Get configuration data from datastores
reply, err := client.GetConfig(ctx, gonetconf.DatastoreRunning, filter)
reply, err := client.GetConfig(ctx, gonetconf.DatastoreCandidate, filter)
reply, err := client.GetConfig(ctx, gonetconf.DatastoreStartup, filter)
// Edit configuration with options
reply, err := client.EditConfig(ctx, gonetconf.DatastoreCandidate, configData,
gonetconf.WithDefaultOperation(gonetconf.DefaultOpMerge),
gonetconf.WithTestOption(gonetconf.TestOpTestThenSet),
gonetconf.WithErrorOption(gonetconf.ErrorOpRollbackOnError),
)
// Copy configuration between datastores
reply, err := client.CopyConfig(ctx, gonetconf.DatastoreRunning, gonetconf.DatastoreCandidate)
// Delete configuration
reply, err := client.DeleteConfig(ctx, gonetconf.DatastoreStartup)
// Lock and unlock datastores
reply, err := client.Lock(ctx, gonetconf.DatastoreCandidate)
reply, err := client.Unlock(ctx, gonetconf.DatastoreCandidate)
// Partial Lock operations (RFC 5717)
// Lock specific subtrees instead of entire datastores
reply, err := client.PartialLock(ctx, []string{
"/interfaces/interface[name='eth0']",
"/vlans/vlan[vlan-id='100']",
})
// Helper methods for common partial lock scenarios
reply, err := client.PartialLockInterface(ctx, "eth0")
reply, err := client.PartialLockVLAN(ctx, "100")
reply, err := client.PartialLockRouting(ctx)
// Unlock using lock ID from partial lock response
reply, err := client.PartialUnlock(ctx, "lock-id-12345")
// Commit changes
reply, err := client.Commit(ctx)
reply, err := client.Commit(ctx, gonetconf.WithConfirmed(300)) // Confirmed commit
// Discard changes
reply, err := client.DiscardChanges(ctx)
// Validate configuration
reply, err := client.Validate(ctx, gonetconf.DatastoreCandidate)
// Kill session
reply, err := client.KillSession(ctx, "session-id")// Subtree filter
filter := gonetconf.NewSubtreeFilter(`
<interfaces xmlns="urn:example:interfaces">
<interface>
<name/>
<oper-status/>
</interface>
</interfaces>
`)
// XPath filter
filter := gonetconf.NewXPathFilter("//interface[name='eth0']")Partial lock allows you to lock specific subtrees of configuration data instead of entire datastores:
// Lock specific configuration subtrees
xpaths := []string{
"/interfaces/interface[name='eth0']",
"/vlans/vlan[vlan-id='100']",
"/routing/static-routes",
}
reply, err := client.PartialLock(ctx, xpaths)
if err != nil {
log.Fatal(err)
}
// Extract lock ID from response for unlock operation
lockID := extractLockID(reply) // You need to parse this from the XML response
// Perform configuration changes on locked subtrees
configData := `
<config>
<interfaces xmlns="urn:example:interfaces">
<interface>
<name>eth0</name>
<description>Updated via partial lock</description>
</interface>
</interfaces>
</config>
`
editReply, err := client.EditConfig(ctx, gonetconf.DatastoreCandidate, configData)
if err != nil {
log.Fatal(err)
}
// Commit changes
commitReply, err := client.Commit(ctx)
if err != nil {
log.Fatal(err)
}
// Release the partial lock
unlockReply, err := client.PartialUnlock(ctx, lockID)
if err != nil {
log.Fatal(err)
}
// Helper methods for common scenarios
reply, err = client.PartialLockInterface(ctx, "eth0") // Locks specific interface
reply, err = client.PartialLockVLAN(ctx, "100") // Locks specific VLAN
reply, err = client.PartialLockRouting(ctx) // Locks routing configurationThe with-defaults capability controls how default values are handled in NETCONF Get and GetConfig operations:
// Using option functions with Get operations
reply, err := client.Get(ctx, filter, gonetconf.WithDefaults(gonetconf.WithDefaultsReport))
reply, err := client.Get(ctx, filter, gonetconf.WithDefaults(gonetconf.WithDefaultsReportAll))
reply, err := client.Get(ctx, filter, gonetconf.WithDefaults(gonetconf.WithDefaultsTrim))
reply, err := client.Get(ctx, filter, gonetconf.WithDefaults(gonetconf.WithDefaultsExplicit))
// Using option functions with GetConfig operations
reply, err := client.GetConfig(ctx, gonetconf.DatastoreRunning, filter,
gonetconf.WithDefaultsConfig(gonetconf.WithDefaultsReport))
// Convenience functions for Get operations
reply, err := client.Get(ctx, filter, gonetconf.WithDefaultsGetReportAll()) // Include all defaults
reply, err := client.Get(ctx, filter, gonetconf.WithDefaultsGetReportAllTagged()) // Include defaults with tags
reply, err := client.Get(ctx, filter, gonetconf.WithDefaultsTrimmed()) // Exclude defaults
reply, err := client.Get(ctx, filter, gonetconf.WithDefaultsExplicitOnly()) // Only explicit values
// Convenience functions for GetConfig operations
reply, err := client.GetConfig(ctx, gonetconf.DatastoreRunning, filter,
gonetconf.WithDefaultsGetConfigReportAll())
reply, err := client.GetConfig(ctx, gonetconf.DatastoreRunning, filter,
gonetconf.WithDefaultsGetConfigReportAllTagged())
reply, err := client.GetConfig(ctx, gonetconf.DatastoreRunning, filter,
gonetconf.WithDefaultsGetConfigTrimmed())
reply, err := client.GetConfig(ctx, gonetconf.DatastoreRunning, filter,
gonetconf.WithDefaultsGetConfigExplicitOnly())
// Direct helper methods
reply, err := client.GetWithDefaults(ctx, filter, gonetconf.WithDefaultsReport)
reply, err := client.GetConfigWithDefaults(ctx, gonetconf.DatastoreRunning, filter, gonetconf.WithDefaultsTrim)
// Specific mode helper methods
reply, err := client.GetReportAll(ctx, filter) // Get with all defaults
reply, err := client.GetConfigReportAll(ctx, gonetconf.DatastoreRunning, filter) // GetConfig with all defaults
reply, err := client.GetTrimmed(ctx, filter) // Get without defaults
reply, err := client.GetConfigTrimmed(ctx, gonetconf.DatastoreRunning, filter) // GetConfig without defaults
reply, err := client.GetExplicit(ctx, filter) // Get explicit values only
reply, err := client.GetConfigExplicit(ctx, gonetconf.DatastoreRunning, filter) // GetConfig explicit values only- report-all: Include all default values in the response
- report-all-tagged: Include default values with special tagging to identify them
- trim: Exclude default values from the response
- explicit: Include only explicitly set values (no defaults)
notificationClient := gonetconf.NewNotificationClient(client)
defer notificationClient.Close()
// Register event handlers
notificationClient.RegisterHandler("*", func(notification *gonetconf.Notification) error {
log.Printf("Received notification: %+v", notification)
return nil
})
notificationClient.RegisterHandler("interface-change", func(notification *gonetconf.Notification) error {
log.Printf("Interface change: %+v", notification.Event)
return nil
})
// Create filter for specific events
filter := gonetconf.NewSubtreeFilter(`
<interfaces xmlns="urn:example:interfaces">
<interface>
<name/>
<oper-status/>
</interface>
</interfaces>
`)
// Subscribe to notifications
subscription, err := notificationClient.Subscribe(ctx, "NETCONF",
gonetconf.WithFilter(filter),
gonetconf.WithStartTime(time.Now()),
)
if err != nil {
log.Fatal(err)
}
// Unsubscribe when done
notificationClient.Unsubscribe(subscription.ID)The library provides specific error types for different failure scenarios:
if err != nil {
switch e := err.(type) {
case *gonetconf.NetconfError:
log.Printf("NETCONF error: %s (type: %s, severity: %s)", e.Error(), e.Type(), e.Severity())
if e.IsFatal() {
log.Printf("Fatal error encountered")
}
case *gonetconf.TransportError:
log.Printf("Transport error: %s", e.Error())
case *gonetconf.AuthenticationError:
log.Printf("Authentication error: %s", e.Error())
case *gonetconf.SessionError:
log.Printf("Session error: %s", e.Error())
default:
log.Printf("Unknown error: %s", e.Error())
}
}The library provides comprehensive constants for NETCONF operations:
gonetconf.DatastoreRunning // "running"
gonetconf.DatastoreCandidate // "candidate"
gonetconf.DatastoreStartup // "startup"gonetconf.DefaultOpMerge // "merge"
gonetconf.DefaultOpReplace // "replace"
gonetconf.DefaultOpCreate // "create"
gonetconf.DefaultOpDelete // "delete"
gonetconf.DefaultOpRemove // "remove"
gonetconf.DefaultOpNone // "none"gonetconf.TestOpTestThenSet // "test-then-set"
gonetconf.TestOpSet // "set"
gonetconf.TestOpTestOnly // "test-only"gonetconf.ErrorOpStopOnError // "stop-on-error"
gonetconf.ErrorOpContinueOnError // "continue-on-error"
gonetconf.ErrorOpRollbackOnError // "rollback-on-error"gonetconf.WithDefaultsReport // "report-all"
gonetconf.WithDefaultsReportAll // "report-all-tagged"
gonetconf.WithDefaultsTrim // "trim"
gonetconf.WithDefaultsExplicit // "explicit"Contributions are welcome! Please ensure that:
- All tests pass with
go test -race ./... - Code follows Go formatting standards with
go fmt - Code passes linting with
golangci-lint run - New features include appropriate tests