Implement BGP MD5 Auth

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)
This commit is contained in:
Ben Roberts
2026-06-17 14:00:52 +01:00
parent 567a84095e
commit d54573e469
4 changed files with 218 additions and 0 deletions

View File

@@ -25,12 +25,14 @@ bgp:
# communities:
# - 100:100
# - 200:200
# md5_env_var: GOCAST_BGP_PEER1_PASSWORD # optional. Set via: export GOCAST_BGP_PEER1_PASSWORD="secret"
# - peer_ip: 10.10.10.2
# peer_as: 6789
# communities:
# - 100:101
# - 200:201
# multi_hop: true # optional
# md5_password: "secret123" # optional
communities:
- asn:nnnn

View File

@@ -23,6 +23,8 @@ type PeerConfig struct {
PeerAS int `yaml:"peer_as"`
MultiHop *bool `yaml:"multi_hop,omitempty"`
Communities []string `yaml:"communities,omitempty"`
MD5Password string `yaml:"md5_password,omitempty"`
MD5EnvVar string `yaml:"md5_env_var,omitempty"`
}
type BgpConfig struct {

View File

@@ -4,6 +4,7 @@ import (
"context"
"fmt"
"net"
"os"
"strconv"
"strings"
@@ -120,9 +121,33 @@ func (c *Controller) addPeer(peer *c.PeerConfig) error {
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 {

View File

@@ -488,3 +488,192 @@ func TestPeerInfoMultiplePeers(t *testing.T) {
a.True(peerAddrs["10.10.10.1"], "Should have first peer")
a.True(peerAddrs["10.10.10.2"], "Should have second peer")
}
func TestMD5Authentication(t *testing.T) {
a := assert.New(t)
testCases := []struct {
name string
md5Password string
md5EnvVar string
envValue string
expectedAuth string
}{
{
name: "MD5 password from config",
md5Password: "secret123",
md5EnvVar: "",
envValue: "",
expectedAuth: "secret123",
},
{
name: "MD5 password from environment variable",
md5Password: "",
md5EnvVar: "BGP_PEER_PASSWORD",
envValue: "env_secret456",
expectedAuth: "env_secret456",
},
{
name: "Environment variable takes priority over config",
md5Password: "config_password",
md5EnvVar: "BGP_PEER_PASSWORD",
envValue: "env_password",
expectedAuth: "env_password",
},
{
name: "No authentication when neither is set",
md5Password: "",
md5EnvVar: "",
envValue: "",
expectedAuth: "",
},
{
name: "Config password used when env var is set but empty",
md5Password: "fallback_password",
md5EnvVar: "EMPTY_VAR",
envValue: "",
expectedAuth: "fallback_password",
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
// Set environment variable if specified
if tc.md5EnvVar != "" && tc.envValue != "" {
os.Setenv(tc.md5EnvVar, tc.envValue)
defer os.Unsetenv(tc.md5EnvVar)
}
ctrl := &Controller{
localAS: 11111,
localIP: net.ParseIP("192.168.1.100"),
s: gobgp.NewBgpServer(),
}
go ctrl.s.Serve()
if err := ctrl.s.StartBgp(context.Background(), &api.StartBgpRequest{
Global: &api.Global{
As: uint32(11111),
RouterId: ctrl.localIP.String(),
ListenPort: -1,
},
}); err != nil {
a.FailNow(err.Error())
}
defer ctrl.s.StopBgp(context.Background(), &api.StopBgpRequest{})
peer := &config.PeerConfig{
PeerIP: "10.10.10.1",
PeerAS: 22222,
MD5Password: tc.md5Password,
MD5EnvVar: tc.md5EnvVar,
}
err := ctrl.addPeer(peer)
a.NoError(err)
// Verify MD5 authentication is configured correctly
var foundPeer *api.Peer
ctrl.s.ListPeer(context.Background(), &api.ListPeerRequest{}, func(p *api.Peer) {
if p.Conf.NeighborAddress == peer.PeerIP {
foundPeer = p
}
})
a.NotNil(foundPeer, "Peer should be added")
a.Equal(tc.expectedAuth, foundPeer.Conf.AuthPassword, "MD5 password should match expected")
})
}
}
func TestGetMD5Password(t *testing.T) {
a := assert.New(t)
ctrl := &Controller{}
// Test 1: Environment variable takes priority
os.Setenv("TEST_BGP_PASS", "env_password")
defer os.Unsetenv("TEST_BGP_PASS")
peer := &config.PeerConfig{
MD5Password: "config_password",
MD5EnvVar: "TEST_BGP_PASS",
}
a.Equal("env_password", ctrl.getMD5Password(peer))
// Test 2: Config password when env var not set
peer2 := &config.PeerConfig{
MD5Password: "config_only",
MD5EnvVar: "NONEXISTENT_VAR",
}
a.Equal("config_only", ctrl.getMD5Password(peer2))
// Test 3: Empty string when nothing is set
peer3 := &config.PeerConfig{}
a.Equal("", ctrl.getMD5Password(peer3))
// Test 4: Only env var specified
os.Setenv("ANOTHER_PASS", "another_env_password")
defer os.Unsetenv("ANOTHER_PASS")
peer4 := &config.PeerConfig{
MD5EnvVar: "ANOTHER_PASS",
}
a.Equal("another_env_password", ctrl.getMD5Password(peer4))
}
func TestMultiPeerWithMD5(t *testing.T) {
a := assert.New(t)
// Set environment variables for testing
os.Setenv("PEER1_PASSWORD", "peer1_secret")
os.Setenv("PEER2_PASSWORD", "peer2_secret")
defer os.Unsetenv("PEER1_PASSWORD")
defer os.Unsetenv("PEER2_PASSWORD")
// Create controller with multiple peers using different MD5 configurations
multiPeerConfig := config.BgpConfig{
LocalAS: 11111,
LocalIP: "192.168.1.100",
Peers: []config.PeerConfig{
{
PeerIP: "10.10.10.1",
PeerAS: 22222,
MD5EnvVar: "PEER1_PASSWORD",
},
{
PeerIP: "10.10.10.2",
PeerAS: 33333,
MD5Password: "peer2_config_password",
},
{
PeerIP: "10.10.10.3",
PeerAS: 44444,
// No MD5 authentication
},
},
Origin: "igp",
}
ctrl, err := NewController(multiPeerConfig)
if err != nil {
a.FailNow(err.Error())
}
defer ctrl.Shutdown()
// Trigger peer addition by announcing a route (peers are added lazily)
_, ipnet, _ := net.ParseCIDR("20.30.40.0/24")
r := &Route{Net: ipnet}
ctrl.Announce(r)
// Verify all peers have correct MD5 configuration
peers := make(map[string]string) // map[peerIP]authPassword
ctrl.s.ListPeer(context.Background(), &api.ListPeerRequest{}, func(p *api.Peer) {
peers[p.Conf.NeighborAddress] = p.Conf.AuthPassword
})
a.Equal(3, len(peers), "Should have all three peers")
a.Equal("peer1_secret", peers["10.10.10.1"], "First peer should use env var password")
a.Equal("peer2_config_password", peers["10.10.10.2"], "Second peer should use config password")
a.Equal("", peers["10.10.10.3"], "Third peer should have no authentication")
}