BGP peers can now be secured with MD5 authentication (TCP MD5 signatures as defined in RFC 2385). This provides an additional layer of security to prevent unauthorized BGP sessions.
MD5 authentication is configured per peer and supports two methods for specifying passwords:
Store passwords in environment variables for better security:
```yaml
bgp:
local_as: 12345
peers:
- peer_ip: 10.10.10.1
peer_as: 6789
md5_env_var: GOCAST_BGP_PEER1_PASSWORD
```
Set the environment variable before starting gocast:
```bash
export GOCAST_BGP_PEER1_PASSWORD="your_secret_password"
./gocast -config config.yaml
```
**Benefits:**
- Passwords not stored in config files
- Easier secret rotation
- Better for containerized deployments (Kubernetes secrets, Docker secrets, etc.)
- Compatible with secret management systems (Vault, AWS Secrets Manager, etc.)
Specify passwords directly in the config file:
```yaml
bgp:
local_as: 12345
peers:
- peer_ip: 10.10.10.1
peer_as: 6789
md5_password: "your_secret_password"
```
**Note:** This method is less secure as passwords are stored in plain text. Only use for testing or when environment variables are not available.
When both `md5_env_var` and `md5_password` are specified, the environment variable takes priority. This allows you to:
- Define a default password in the config
- Override it with an environment variable in production
- Use different passwords per environment without changing config files
Different peers can use different authentication methods:
```yaml
bgp:
local_as: 12345
peers:
# Peer 1: Environment variable
- peer_ip: 10.10.10.1
peer_as: 6789
md5_env_var: GOCAST_BGP_PEER1_PASSWORD
# Peer 2: Config file password
- peer_ip: 10.10.10.2
peer_as: 6789
md5_password: "fallback_password"
# Peer 3: No authentication
- peer_ip: 10.10.10.3
peer_as: 6789
```
Recommended naming patterns:
```bash
export GOCAST_BGP_PRIMARY_PEER_PASSWORD="secret1"
export GOCAST_BGP_SECONDARY_PEER_PASSWORD="secret2"
export GOCAST_BGP_10_10_10_1_PASSWORD="secret1"
export GOCAST_BGP_10_10_10_2_PASSWORD="secret2"
export GOCAST_BGP_AS6789_PASSWORD="secret1"
```
**config/config.go**
- Added `MD5Password` field to `PeerConfig` for config file passwords
- Added `MD5EnvVar` field to `PeerConfig` for environment variable references
**controller/bgp.go**
- Added `getMD5Password()` helper function to retrieve passwords
- Modified `addPeer()` to configure MD5 authentication when available
- Environment variable lookup prioritizes env vars over config passwords
Comprehensive test suite covering:
- MD5 password from config file
- MD5 password from environment variable
- Environment variable priority over config
- No authentication scenario
- Fallback to config when env var is empty
- Multiple peers with mixed authentication methods
This commit was written using AI LLM
Authored-By: Claude Code (Sonnet 4.5)
299 lines
7.1 KiB
Go
299 lines
7.1 KiB
Go
package controller
|
|
|
|
import (
|
|
"context"
|
|
"fmt"
|
|
"net"
|
|
"os"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"github.com/golang/protobuf/ptypes"
|
|
"github.com/golang/protobuf/ptypes/any"
|
|
c "github.com/mayuresh82/gocast/config"
|
|
api "github.com/osrg/gobgp/api"
|
|
gobgp "github.com/osrg/gobgp/pkg/server"
|
|
)
|
|
|
|
type Route struct {
|
|
Net *net.IPNet
|
|
Communities []string
|
|
}
|
|
|
|
type Controller struct {
|
|
localAS int
|
|
localIP net.IP
|
|
peers []c.PeerConfig
|
|
communities []string
|
|
origin uint32
|
|
s *gobgp.BgpServer
|
|
}
|
|
|
|
func NewController(config c.BgpConfig) (*Controller, error) {
|
|
ctrl := &Controller{}
|
|
var gw net.IP
|
|
var err error
|
|
|
|
// Normalize config: convert legacy single-peer to new multi-peer format
|
|
peers := config.Peers
|
|
if len(peers) == 0 {
|
|
// Backward compatibility: convert legacy config
|
|
if config.PeerIP != "" {
|
|
// Explicit peer IP configured
|
|
peers = []c.PeerConfig{{
|
|
PeerIP: config.PeerIP,
|
|
PeerAS: config.PeerAS,
|
|
}}
|
|
} else {
|
|
// No peer IP configured - use default gateway
|
|
gw, err = gateway()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Unable to get gateway ip: %v", err)
|
|
}
|
|
peers = []c.PeerConfig{{
|
|
PeerIP: gw.String(),
|
|
PeerAS: config.PeerAS,
|
|
}}
|
|
}
|
|
}
|
|
|
|
// Determine local IP
|
|
if config.LocalIP == "" {
|
|
// Use first peer to determine local IP
|
|
firstPeerIP := net.ParseIP(peers[0].PeerIP)
|
|
if firstPeerIP == nil {
|
|
gw, err = gateway()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Unable to get gw ip: %v", err)
|
|
}
|
|
firstPeerIP = gw
|
|
}
|
|
gw, err = via(firstPeerIP)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Unable to get gw ip: %v", err)
|
|
}
|
|
ctrl.localIP, err = localAddress(gw)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
} else {
|
|
ctrl.localIP = net.ParseIP(config.LocalIP)
|
|
}
|
|
|
|
ctrl.localAS = config.LocalAS
|
|
ctrl.peers = peers
|
|
ctrl.communities = config.Communities
|
|
|
|
switch config.Origin {
|
|
case "igp":
|
|
ctrl.origin = 0
|
|
case "egp":
|
|
ctrl.origin = 1
|
|
case "unknown":
|
|
ctrl.origin = 2
|
|
}
|
|
|
|
s := gobgp.NewBgpServer()
|
|
go s.Serve()
|
|
if err := s.StartBgp(context.Background(), &api.StartBgpRequest{
|
|
Global: &api.Global{
|
|
As: uint32(config.LocalAS),
|
|
RouterId: ctrl.localIP.String(),
|
|
ListenPort: -1, // gobgp won't listen on tcp:179
|
|
},
|
|
}); err != nil {
|
|
return nil, fmt.Errorf("Unable to start bgp: %v", err)
|
|
}
|
|
ctrl.s = s
|
|
|
|
return ctrl, nil
|
|
}
|
|
|
|
func (c *Controller) addPeer(peer *c.PeerConfig) error {
|
|
n := &api.Peer{
|
|
Conf: &api.PeerConf{
|
|
NeighborAddress: peer.PeerIP,
|
|
PeerAs: uint32(peer.PeerAS),
|
|
},
|
|
}
|
|
|
|
// Enable multihop only if explicitly configured
|
|
if peer.MultiHop != nil && *peer.MultiHop {
|
|
n.EbgpMultihop = &api.EbgpMultihop{Enabled: true, MultihopTtl: uint32(255)}
|
|
}
|
|
|
|
// Configure MD5 authentication if specified
|
|
md5Password := c.getMD5Password(peer)
|
|
if md5Password != "" {
|
|
n.Conf.AuthPassword = md5Password
|
|
}
|
|
|
|
return c.s.AddPeer(context.Background(), &api.AddPeerRequest{Peer: n})
|
|
}
|
|
|
|
// getMD5Password retrieves the MD5 password from config or environment variable
|
|
func (c *Controller) getMD5Password(peer *c.PeerConfig) string {
|
|
// Priority 1: Check for environment variable
|
|
if peer.MD5EnvVar != "" {
|
|
if password := os.Getenv(peer.MD5EnvVar); password != "" {
|
|
return password
|
|
}
|
|
}
|
|
|
|
// Priority 2: Use password from config file
|
|
if peer.MD5Password != "" {
|
|
return peer.MD5Password
|
|
}
|
|
|
|
return ""
|
|
}
|
|
|
|
func (c *Controller) getApiPath(route *Route, peer *c.PeerConfig) *api.Path {
|
|
afi := api.Family_AFI_IP
|
|
if route.Net.IP.To4() == nil {
|
|
afi = api.Family_AFI_IP6
|
|
}
|
|
prefixlen, _ := route.Net.Mask.Size()
|
|
nlri, _ := ptypes.MarshalAny(&api.IPAddressPrefix{
|
|
Prefix: route.Net.IP.String(),
|
|
PrefixLen: uint32(prefixlen),
|
|
})
|
|
a1, _ := ptypes.MarshalAny(&api.OriginAttribute{
|
|
Origin: c.origin,
|
|
})
|
|
a2, _ := ptypes.MarshalAny(&api.NextHopAttribute{
|
|
NextHop: c.localIP.String(),
|
|
})
|
|
|
|
// Merge communities: global + per-peer + per-route
|
|
var allCommunities []string
|
|
allCommunities = append(allCommunities, c.communities...)
|
|
allCommunities = append(allCommunities, peer.Communities...)
|
|
allCommunities = append(allCommunities, route.Communities...)
|
|
|
|
var communities []uint32
|
|
for _, comm := range allCommunities {
|
|
communities = append(communities, convertCommunity(comm))
|
|
}
|
|
a3, _ := ptypes.MarshalAny(&api.CommunitiesAttribute{
|
|
Communities: communities,
|
|
})
|
|
attrs := []*any.Any{a1, a2, a3}
|
|
return &api.Path{
|
|
Family: &api.Family{Afi: afi, Safi: api.Family_SAFI_UNICAST},
|
|
Nlri: nlri,
|
|
Pattrs: attrs,
|
|
}
|
|
}
|
|
|
|
func (c *Controller) Announce(route *Route) error {
|
|
var errs []error
|
|
|
|
for i := range c.peers {
|
|
peer := &c.peers[i]
|
|
|
|
// Check if peer exists
|
|
var found bool
|
|
err := c.s.ListPeer(context.Background(), &api.ListPeerRequest{}, func(p *api.Peer) {
|
|
if p.Conf.NeighborAddress == peer.PeerIP {
|
|
found = true
|
|
}
|
|
})
|
|
if err != nil {
|
|
errs = append(errs, fmt.Errorf("peer %s: list error: %v", peer.PeerIP, err))
|
|
continue
|
|
}
|
|
|
|
// Add peer if not found
|
|
if !found {
|
|
if err := c.addPeer(peer); err != nil {
|
|
errs = append(errs, fmt.Errorf("peer %s: add error: %v", peer.PeerIP, err))
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Announce route to this peer
|
|
path := c.getApiPath(route, peer)
|
|
if _, err := c.s.AddPath(context.Background(), &api.AddPathRequest{Path: path}); err != nil {
|
|
errs = append(errs, fmt.Errorf("peer %s: announce error: %v", peer.PeerIP, err))
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Return aggregated errors if any peer failed
|
|
if len(errs) > 0 {
|
|
return fmt.Errorf("announcement errors: %v", errs)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Controller) Withdraw(route *Route) error {
|
|
var errs []error
|
|
|
|
for i := range c.peers {
|
|
peer := &c.peers[i]
|
|
path := c.getApiPath(route, peer)
|
|
if err := c.s.DeletePath(context.Background(), &api.DeletePathRequest{Path: path}); err != nil {
|
|
errs = append(errs, fmt.Errorf("peer %s: withdraw error: %v", peer.PeerIP, err))
|
|
continue
|
|
}
|
|
}
|
|
|
|
// Return aggregated errors if any peer failed
|
|
if len(errs) > 0 {
|
|
return fmt.Errorf("withdrawal errors: %v", errs)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (c *Controller) PeerInfo() ([]*api.Peer, error) {
|
|
var peers []*api.Peer
|
|
peerMap := make(map[string]bool)
|
|
|
|
// Build map of configured peer IPs
|
|
for _, peer := range c.peers {
|
|
peerMap[peer.PeerIP] = true
|
|
}
|
|
|
|
// Collect info for all configured peers
|
|
err := c.s.ListPeer(context.Background(), &api.ListPeerRequest{}, func(p *api.Peer) {
|
|
if peerMap[p.Conf.NeighborAddress] {
|
|
peers = append(peers, p)
|
|
}
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return peers, nil
|
|
}
|
|
|
|
func (c *Controller) Shutdown() error {
|
|
var errs []error
|
|
|
|
// Shutdown all peer sessions
|
|
for _, peer := range c.peers {
|
|
if err := c.s.ShutdownPeer(context.Background(), &api.ShutdownPeerRequest{
|
|
Address: peer.PeerIP,
|
|
}); err != nil {
|
|
errs = append(errs, fmt.Errorf("peer %s: shutdown error: %v", peer.PeerIP, err))
|
|
}
|
|
}
|
|
|
|
// Stop BGP server
|
|
if err := c.s.StopBgp(context.Background(), &api.StopBgpRequest{}); err != nil {
|
|
errs = append(errs, fmt.Errorf("stop bgp error: %v", err))
|
|
}
|
|
|
|
if len(errs) > 0 {
|
|
return fmt.Errorf("shutdown errors: %v", errs)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func convertCommunity(comm string) uint32 {
|
|
parts := strings.Split(comm, ":")
|
|
first, _ := strconv.ParseUint(parts[0], 10, 32)
|
|
second, _ := strconv.ParseUint(parts[1], 10, 32)
|
|
return uint32(first)<<16 | uint32(second)
|
|
}
|