2 Commits

Author SHA1 Message Date
mgaitonde
cdc61f713a add matchtag and fix tests 2023-08-18 17:41:51 -07:00
mgaitonde
645b10548a ipv6 support wip 2023-08-18 16:23:47 -07:00
12 changed files with 173 additions and 832 deletions

View File

@@ -5,9 +5,6 @@ The most common use case for this is anycast (vip) based load balancing for infr
For some practical examples and more details, check out this blog post : https://mayuresh82.github.io/2020/11/28/automatic_service_discovery_anycast/
# Looking for code reviewers
If you are interested in being a reviewer and/or co-maintainer, please reach out to @mayuresh82 !
## Installation
Use the docker container at mayuresh82/gocast or compile from source:

View File

@@ -11,33 +11,25 @@ agent:
consul_query_interval: 5m
# token to authenticate client if consul requires it
consul_token: 00000000-0000-0000-0000-000000000000
# this tag must be present in consul for Gocast to pickup the service
# by default its enable_gocast
consul_match_tag: enable_gocast
bgp:
local_as: 12345
remote_as: 6789
# override the peer IP to use instead of auto discovering
peer_ip: 10.10.10.1
# Alternatively, define multiple BGP peers for redundancy
#peers:
# - peer_ip: 10.10.10.1
# peer_as: 6789
# 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
- asn:nnnn
origin: igp
# family will determine whether we want to peer over an ipv4 or ipv6 session
# one gocast instance can only peer over one session. Multiple sessions are not
# supported. The default is 4
family: 4
# router ID is required when family 6 is specified
router_id: 1.1.1.1
# optional list of apps to register on startup
apps:
@@ -46,5 +38,4 @@ apps:
vip_config:
# additional per VIP BGP communities
bgp_communities: [ aaaa:bbbb ]
monitors:
- port:tcp:5000
monitor: port:tcp:5000

View File

@@ -16,27 +16,18 @@ type AgentConfig struct {
ConsulAddr string `yaml:"consul_addr"`
ConsulQueryInterval time.Duration `yaml:"consul_query_interval"`
ConsulToken string `yaml:"consul_token"`
}
type PeerConfig struct {
PeerIP string `yaml:"peer_ip"`
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"`
ConsulMatchTag string `yaml:"consul_match_tag"`
}
type BgpConfig struct {
LocalAS int `yaml:"local_as"`
PeerAS int `yaml:"peer_as"`
LocalIP string `yaml:"local_ip"`
// Legacy single-peer config (deprecated but supported for backward compatibility)
PeerAS int `yaml:"peer_as,omitempty"`
PeerIP string `yaml:"peer_ip,omitempty"`
// New multi-peer config
Peers []PeerConfig `yaml:"peers,omitempty"`
PeerIP string `yaml:"peer_ip"`
Communities []string
Origin string
Family int
RouterID string `yaml:"router_id"`
}
type VipConfig struct {

View File

@@ -4,7 +4,6 @@ import (
"context"
"fmt"
"net"
"os"
"strconv"
"strings"
@@ -21,134 +20,92 @@ type Route struct {
}
type Controller struct {
localAS int
localIP net.IP
peers []c.PeerConfig
peerAS int
localIP, peerIP net.IP
communities []string
origin uint32
multiHop bool
s *gobgp.BgpServer
}
func NewController(config c.BgpConfig) (*Controller, error) {
ctrl := &Controller{}
c := &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,
}}
if config.Family == 0 {
config.Family = 4
}
if config.PeerIP == "" {
gw, err := gateway(config.Family)
if err != nil {
return nil, fmt.Errorf("unable to get gw ip: %v", err)
}
c.peerIP = gw
} 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)
c.peerIP = net.ParseIP(config.PeerIP)
}
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()
gw, err = via(c.peerIP)
if err != nil {
return nil, fmt.Errorf("Unable to get gw ip: %v", err)
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)
c.localIP, err = localAddress(gw)
if err != nil {
return nil, err
}
} else {
ctrl.localIP = net.ParseIP(config.LocalIP)
c.localIP = net.ParseIP(config.LocalIP)
}
ctrl.localAS = config.LocalAS
ctrl.peers = peers
ctrl.communities = config.Communities
c.communities = config.Communities
switch config.Origin {
case "igp":
ctrl.origin = 0
c.origin = 0
case "egp":
ctrl.origin = 1
c.origin = 1
case "unknown":
ctrl.origin = 2
c.origin = 2
}
var rid string
if config.Family == 4 {
rid = c.localIP.String()
}
if config.RouterID != "" {
rid = config.RouterID
}
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(),
RouterId: rid,
ListenPort: -1, // gobgp won't listen on tcp:179
},
}); err != nil {
return nil, fmt.Errorf("Unable to start bgp: %v", err)
return nil, fmt.Errorf("unable to start bgp: %v", err)
}
ctrl.s = s
return ctrl, nil
c.s = s
c.peerAS = config.PeerAS
// set mh by default for all ebgp peers
if c.peerAS != config.LocalAS {
c.multiHop = true
}
return c, nil
}
func (c *Controller) addPeer(peer *c.PeerConfig) error {
func (c *Controller) AddPeer(peer string) error {
n := &api.Peer{
Conf: &api.PeerConf{
NeighborAddress: peer.PeerIP,
PeerAs: uint32(peer.PeerAS),
NeighborAddress: peer,
PeerAs: uint32(c.peerAS),
},
}
// Enable multihop only if explicitly configured
if peer.MultiHop != nil && *peer.MultiHop {
if c.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 {
func (c *Controller) getApiPath(route *Route) *api.Path {
afi := api.Family_AFI_IP
if route.Net.IP.To4() == nil {
afi = api.Family_AFI_IP6
@@ -164,15 +121,8 @@ func (c *Controller) getApiPath(route *Route, peer *c.PeerConfig) *api.Path {
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 {
for _, comm := range append(c.communities, route.Communities...) {
communities = append(communities, convertCommunity(comm))
}
a3, _ := ptypes.MarshalAny(&api.CommunitiesAttribute{
@@ -187,105 +137,49 @@ func (c *Controller) getApiPath(route *Route, peer *c.PeerConfig) *api.Path {
}
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 {
if p.Conf.NeighborAddress == c.peerIP.String() {
found = true
}
})
if err != nil {
errs = append(errs, fmt.Errorf("peer %s: list error: %v", peer.PeerIP, err))
continue
return err
}
// 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
if err := c.AddPeer(c.peerIP.String()); err != nil {
return err
}
}
// 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
_, err = c.s.AddPath(context.Background(), &api.AddPathRequest{Path: c.getApiPath(route)})
return err
}
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 c.s.DeletePath(context.Background(), &api.DeletePathRequest{Path: c.getApiPath(route)})
}
// 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
func (c *Controller) PeerInfo() (*api.Peer, error) {
var peer *api.Peer
err := c.s.ListPeer(context.Background(), &api.ListPeerRequest{}, func(p *api.Peer) {
if peerMap[p.Conf.NeighborAddress] {
peers = append(peers, p)
if p.Conf.NeighborAddress == c.peerIP.String() {
peer = p
}
})
if err != nil {
return nil, err
}
return peers, nil
return peer, 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,
Address: c.peerIP.String(),
}); err != nil {
errs = append(errs, fmt.Errorf("peer %s: shutdown error: %v", peer.PeerIP, err))
return 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 err
}
return nil
}

View File

@@ -102,578 +102,3 @@ func TestBgpNew(t *testing.T) {
a.Equal("20.30.40.0/24", path)
ctrl.Shutdown()
}
func TestLegacyConfigConversion(t *testing.T) {
a := assert.New(t)
// Test legacy single-peer config
legacyConfig := config.BgpConfig{
LocalAS: 11111,
PeerAS: 22222,
PeerIP: "10.10.10.1",
LocalIP: "192.168.1.100",
Communities: []string{"100:100"},
Origin: "igp",
}
ctrl, err := NewController(legacyConfig)
if err != nil {
a.FailNow(err.Error())
}
defer ctrl.Shutdown()
// Verify legacy config was converted to multi-peer format
a.Equal(1, len(ctrl.peers), "Should have exactly 1 peer")
a.Equal("10.10.10.1", ctrl.peers[0].PeerIP)
a.Equal(22222, ctrl.peers[0].PeerAS)
}
func TestMultiPeerConfig(t *testing.T) {
a := assert.New(t)
// Test new multi-peer config
multiPeerConfig := config.BgpConfig{
LocalAS: 11111,
LocalIP: "192.168.1.100",
Peers: []config.PeerConfig{
{PeerIP: "10.10.10.1", PeerAS: 22222},
{PeerIP: "10.10.10.2", PeerAS: 22222},
},
Communities: []string{"100:100"},
Origin: "igp",
}
ctrl, err := NewController(multiPeerConfig)
if err != nil {
a.FailNow(err.Error())
}
defer ctrl.Shutdown()
// Verify both peers are configured
a.Equal(2, len(ctrl.peers), "Should have exactly 2 peers")
a.Equal("10.10.10.1", ctrl.peers[0].PeerIP)
a.Equal("10.10.10.2", ctrl.peers[1].PeerIP)
}
func TestDefaultGatewayPeer(t *testing.T) {
a := assert.New(t)
// Test config with no peer_ip - should use default gateway
defaultGatewayConfig := config.BgpConfig{
LocalAS: 11111,
LocalIP: "192.168.1.100",
PeerAS: 22222,
Origin: "igp",
}
ctrl, err := NewController(defaultGatewayConfig)
a.NoError(err, "Should not error when peer_ip is not specified")
if err == nil {
defer ctrl.Shutdown()
// Verify a peer was configured using gateway
a.Equal(1, len(ctrl.peers), "Should have exactly 1 peer")
a.NotEmpty(ctrl.peers[0].PeerIP, "Peer IP should be set from gateway")
a.Equal(22222, ctrl.peers[0].PeerAS, "Peer AS should match config")
}
}
func TestCommunityMerging(t *testing.T) {
a := assert.New(t)
ctrl := &Controller{
localAS: 11111,
localIP: net.ParseIP("192.168.1.100"),
communities: []string{"1000:1000", "2000:2000"}, // Global
origin: 0,
}
peer := &config.PeerConfig{
PeerIP: "10.10.10.1",
PeerAS: 22222,
Communities: []string{"100:100", "200:200"}, // Per-peer
}
route := &Route{
Net: &net.IPNet{
IP: net.ParseIP("20.30.40.0"),
Mask: net.CIDRMask(24, 32),
},
Communities: []string{"5000:5000"}, // Per-route
}
path := ctrl.getApiPath(route, peer)
// Extract communities from path
var commAttr *api.CommunitiesAttribute
for _, attr := range path.Pattrs {
var dynAny ptypes.DynamicAny
if err := ptypes.UnmarshalAny(attr, &dynAny); err == nil {
if c, ok := dynAny.Message.(*api.CommunitiesAttribute); ok {
commAttr = c
break
}
}
}
a.NotNil(commAttr, "Should have communities attribute")
a.Equal(5, len(commAttr.Communities), "Should have 5 communities (2 global + 2 peer + 1 route)")
// Verify community values (converted to uint32)
expectedCommunities := []uint32{
convertCommunity("1000:1000"), // Global
convertCommunity("2000:2000"), // Global
convertCommunity("100:100"), // Per-peer
convertCommunity("200:200"), // Per-peer
convertCommunity("5000:5000"), // Per-route
}
a.Equal(expectedCommunities, commAttr.Communities)
}
func TestMultiHopConfiguration(t *testing.T) {
a := assert.New(t)
testCases := []struct {
name string
localAS int
peerAS int
multiHopPtr *bool
expectMH bool
}{
{
name: "default - multihop not enabled",
localAS: 11111,
peerAS: 22222,
multiHopPtr: nil,
expectMH: false,
},
{
name: "explicit disable",
localAS: 11111,
peerAS: 22222,
multiHopPtr: boolPtr(false),
expectMH: false,
},
{
name: "explicit enable",
localAS: 11111,
peerAS: 22222,
multiHopPtr: boolPtr(true),
expectMH: true,
},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ctrl := &Controller{
localAS: tc.localAS,
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(tc.localAS),
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: tc.peerAS,
MultiHop: tc.multiHopPtr,
}
err := ctrl.addPeer(peer)
a.NoError(err)
// Verify multihop setting by checking peer config
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")
if tc.expectMH {
a.NotNil(foundPeer.EbgpMultihop, "Should have multihop configured")
a.True(foundPeer.EbgpMultihop.Enabled, "Multihop should be enabled")
} else {
if foundPeer.EbgpMultihop != nil {
a.False(foundPeer.EbgpMultihop.Enabled, "Multihop should not be enabled")
}
}
})
}
}
// Helper function to create bool pointer
func boolPtr(b bool) *bool {
return &b
}
func TestMultiPeerAnnouncement(t *testing.T) {
if os.Getenv("CI") != "" {
t.Skip("Skipping testing in CI environment")
}
a := assert.New(t)
// Create two BGP listeners
listener1, err := NewBgpListener(22222)
if err != nil {
panic(err)
}
defer listener1.Shutdown()
listener2, err := NewBgpListener(33333)
if err != nil {
panic(err)
}
defer listener2.Shutdown()
// Create controller with two peers
multiPeerConfig := config.BgpConfig{
LocalAS: 11111,
LocalIP: "192.168.1.100",
Peers: []config.PeerConfig{
{PeerIP: "127.0.0.1", PeerAS: 22222},
{PeerIP: "127.0.0.1", PeerAS: 33333},
},
Communities: []string{"100:100"},
Origin: "igp",
}
ctrl, err := NewController(multiPeerConfig)
if err != nil {
a.FailNow(err.Error())
}
defer ctrl.Shutdown()
// Announce a route
_, ipnet, _ := net.ParseCIDR("20.30.40.0/24")
r := &Route{Net: ipnet}
if err := ctrl.Announce(r); err != nil {
a.FailNow(err.Error())
}
// Verify both listeners received the route
path1 := <-listener1.recvdPaths
a.Equal("20.30.40.0/24", path1)
path2 := <-listener2.recvdPaths
a.Equal("20.30.40.0/24", path2)
}
func TestBestEffortAnnouncement(t *testing.T) {
a := assert.New(t)
// Create controller with two peers
mixedConfig := config.BgpConfig{
LocalAS: 11111,
LocalIP: "192.168.1.100",
Peers: []config.PeerConfig{
{PeerIP: "127.0.0.1", PeerAS: 22222},
{PeerIP: "127.0.0.2", PeerAS: 33333},
},
Origin: "igp",
}
ctrl, err := NewController(mixedConfig)
if err != nil {
a.FailNow(err.Error())
}
defer ctrl.Shutdown()
// Announce a route - both peers will be added successfully
// (they won't have actual BGP sessions established, but peers are added to GoBGP)
_, ipnet, _ := net.ParseCIDR("20.30.40.0/24")
r := &Route{Net: ipnet}
// The announcement should succeed for both peers being added
err = ctrl.Announce(r)
a.NoError(err, "Announcement should succeed for both peers")
// Verify both peers were added
peers, err := ctrl.PeerInfo()
a.NoError(err)
a.Equal(2, len(peers), "Should have both peers configured")
}
func TestWithdrawMultiplePeers(t *testing.T) {
a := assert.New(t)
ctrl := &Controller{
localAS: 11111,
localIP: net.ParseIP("192.168.1.100"),
peers: []config.PeerConfig{
{PeerIP: "10.10.10.1", PeerAS: 22222},
{PeerIP: "10.10.10.2", PeerAS: 22222},
},
origin: 0,
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{})
_, ipnet, _ := net.ParseCIDR("20.30.40.0/24")
r := &Route{Net: ipnet}
// Withdraw should iterate through all peers
// This will fail because peers aren't established, but it should try both
err := ctrl.Withdraw(r)
// We expect an error but it should have tried both peers
if err != nil {
a.Contains(err.Error(), "withdrawal errors")
}
}
func TestPeerInfoMultiplePeers(t *testing.T) {
a := assert.New(t)
ctrl := &Controller{
localAS: 11111,
localIP: net.ParseIP("192.168.1.100"),
peers: []config.PeerConfig{
{PeerIP: "10.10.10.1", PeerAS: 22222},
{PeerIP: "10.10.10.2", PeerAS: 33333},
},
origin: 0,
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{})
// Add peers
for i := range ctrl.peers {
ctrl.addPeer(&ctrl.peers[i])
}
// Get peer info
peers, err := ctrl.PeerInfo()
a.NoError(err)
a.Equal(2, len(peers), "Should return info for both peers")
// Verify peer addresses
peerAddrs := make(map[string]bool)
for _, p := range peers {
peerAddrs[p.Conf.NeighborAddress] = true
}
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")
}

View File

@@ -17,7 +17,7 @@ const (
consulNodeEnv = "CONSUL_NODE"
consulToken = "CONSUL_TOKEN"
allowStale = "CONSUL_STALE"
matchTag = "enable_gocast"
defaultmatchTag = "enable_gocast"
nodeURL = "/catalog/node"
remoteHealthCheckurl = "/health/checks"
localHealthCheckurl = "/agent/checks"
@@ -35,6 +35,7 @@ type ConsulMon struct {
addr string
token string
node string
matchTag string
client Clienter
}
@@ -55,12 +56,15 @@ func contains(inp []string, elem string) bool {
return false
}
func NewConsulMon(addr string, token string) (*ConsulMon, error) {
func NewConsulMon(addr string, token string, matchTag string) (*ConsulMon, error) {
node := os.Getenv(consulNodeEnv)
if node == "" {
return nil, fmt.Errorf("%s env variable not set", consulNodeEnv)
}
return &ConsulMon{addr: addr, token: token, node: node, client: &http.Client{Timeout: 10 * time.Second}}, nil
if matchTag == "" {
matchTag = defaultmatchTag
}
return &ConsulMon{addr: addr, token: token, node: node, client: &http.Client{Timeout: 10 * time.Second}, matchTag: matchTag}, nil
}
func getHTTPReq(httpMethod string, addr string, tokenFrmCfg string) (*http.Request, error) {
@@ -95,10 +99,10 @@ func (c *ConsulMon) queryServices() ([]*App, error) {
defer resp.Body.Close()
var consulData ConsulServiceData
if err := json.NewDecoder(resp.Body).Decode(&consulData); err != nil {
return apps, fmt.Errorf("Unable to decode consul data: %v", err)
return apps, fmt.Errorf("unable to decode consul data: %v", err)
}
for _, service := range consulData.Services {
if !contains(service.Tags, matchTag) {
if !contains(service.Tags, c.matchTag) {
continue
}
var (
@@ -170,7 +174,7 @@ func (c *ConsulMon) healthCheckLocal(service string) (bool, error) {
return false, nil
}
}
return false, fmt.Errorf("No local healthcheck info found for service %s on node %s in consul", service, c.node)
return false, fmt.Errorf("no local healthcheck info found for service %s on node %s in consul", service, c.node)
}
// healthCheckRemote queries the consul cluster's healthcheck endpoint to perform service healthchecks
@@ -202,7 +206,7 @@ func (c *ConsulMon) healthCheckRemote(service string) (bool, error) {
return false, nil
}
}
return false, fmt.Errorf("No healthcheck info found for node %s in consul", c.node)
return false, fmt.Errorf("no healthcheck info found for node %s in consul", c.node)
}
// healthCheck determines if we should use the local agent

View File

@@ -2,7 +2,7 @@ package controller
import (
"bytes"
"io/ioutil"
"io"
"net/http"
"os"
"testing"
@@ -119,13 +119,13 @@ func TestQueryServices(t *testing.T) {
a := assert.New(t)
client := &MockClient{}
cm := &ConsulMon{
addr: "foo", node: "test", client: client,
addr: "foo", node: "test", client: client, matchTag: "enable_gocast",
}
// test valid app
client.do = func(*http.Request) (*http.Response, error) {
b := bytes.NewBuffer([]byte(mockConsulData["single-app"]))
return &http.Response{Body: ioutil.NopCloser(b), StatusCode: http.StatusOK}, nil
return &http.Response{Body: io.NopCloser(b), StatusCode: http.StatusOK}, nil
}
apps, err := cm.queryServices()
if err != nil {
@@ -140,7 +140,7 @@ func TestQueryServices(t *testing.T) {
// test no match
client.do = func(*http.Request) (*http.Response, error) {
b := bytes.NewBuffer([]byte(mockConsulData["single-app-no-match"]))
return &http.Response{Body: ioutil.NopCloser(b), StatusCode: http.StatusOK}, nil
return &http.Response{Body: io.NopCloser(b), StatusCode: http.StatusOK}, nil
}
apps, err = cm.queryServices()
if err != nil {
@@ -151,7 +151,7 @@ func TestQueryServices(t *testing.T) {
// test missing vip
client.do = func(*http.Request) (*http.Response, error) {
b := bytes.NewBuffer([]byte(mockConsulData["single-app-no-vip"]))
return &http.Response{Body: ioutil.NopCloser(b), StatusCode: http.StatusOK}, nil
return &http.Response{Body: io.NopCloser(b), StatusCode: http.StatusOK}, nil
}
apps, _ = cm.queryServices()
a.Equal(0, len(apps))
@@ -160,13 +160,13 @@ func TestQueryServices(t *testing.T) {
func TestHealthCheck(t *testing.T) {
a := assert.New(t)
client := &MockClient{}
cm := &ConsulMon{node: "test-node1", client: client}
cm := &ConsulMon{node: "test-node1", client: client, matchTag: "enable_gocast"}
// test remote checks
cm.addr = "http://remote/check"
client.do = func(*http.Request) (*http.Response, error) {
b := bytes.NewBuffer([]byte(mockConsulCheckData["remote-pass"]))
return &http.Response{Body: ioutil.NopCloser(b), StatusCode: http.StatusOK}, nil
return &http.Response{Body: io.NopCloser(b), StatusCode: http.StatusOK}, nil
}
check, err := cm.healthCheck("test-service")
if err != nil {
@@ -175,7 +175,7 @@ func TestHealthCheck(t *testing.T) {
a.True(check)
client.do = func(*http.Request) (*http.Response, error) {
b := bytes.NewBuffer([]byte(mockConsulCheckData["remote-fail"]))
return &http.Response{Body: ioutil.NopCloser(b), StatusCode: http.StatusOK}, nil
return &http.Response{Body: io.NopCloser(b), StatusCode: http.StatusOK}, nil
}
check, _ = cm.healthCheck("test-service")
a.False(check)
@@ -184,7 +184,7 @@ func TestHealthCheck(t *testing.T) {
cm.addr = "http://localhost/check"
client.do = func(*http.Request) (*http.Response, error) {
b := bytes.NewBuffer([]byte(mockConsulCheckData["local-pass"]))
return &http.Response{Body: ioutil.NopCloser(b), StatusCode: http.StatusOK}, nil
return &http.Response{Body: io.NopCloser(b), StatusCode: http.StatusOK}, nil
}
check, _ = cm.healthCheck("test-service")
if err != nil {
@@ -194,7 +194,7 @@ func TestHealthCheck(t *testing.T) {
cm.addr = "http://127.0.0.1/check"
client.do = func(*http.Request) (*http.Response, error) {
b := bytes.NewBuffer([]byte(mockConsulCheckData["local-fail"]))
return &http.Response{Body: ioutil.NopCloser(b), StatusCode: http.StatusOK}, nil
return &http.Response{Body: io.NopCloser(b), StatusCode: http.StatusOK}, nil
}
check, _ = cm.healthCheck("test-service")
a.False(check)

View File

@@ -84,7 +84,7 @@ func NewMonitor(config *c.Config) *MonitorMgr {
cleanups: make(map[string]chan bool),
}
if config.Agent.ConsulAddr != "" {
cmon, err := NewConsulMon(config.Agent.ConsulAddr, config.Agent.ConsulToken)
cmon, err := NewConsulMon(config.Agent.ConsulAddr, config.Agent.ConsulToken, config.Agent.ConsulMatchTag)
if err != nil {
glog.Errorf("Failed to start consul monitor: %v", err)
} else {
@@ -356,6 +356,6 @@ func (m *MonitorMgr) Cleanup(app string, exit chan bool) {
}
// GetInfo returns basic BGP info for established peers
func (m *MonitorMgr) GetInfo() ([]*api.Peer, error) {
func (m *MonitorMgr) GetInfo() (*api.Peer, error) {
return m.ctrl.PeerInfo()
}

View File

@@ -18,22 +18,30 @@ func getCmdList(mainCmd string) []string {
return cmdList
}
func gateway() (net.IP, error) {
cmd := `ip route | grep "^default" | cut -d" " -f3`
func gateway(family int) (net.IP, error) {
prefix := "ip"
if family == 6 {
prefix = "ip -6"
}
cmd := fmt.Sprintf(`%s route | grep "^default" | cut -d" " -f3`, prefix)
cmdList := getCmdList(cmd)
out, err := exec.Command(execCmd, cmdList...).Output()
if err != nil {
return nil, fmt.Errorf("Failed to execute command: %s: %v", cmd, err)
return nil, fmt.Errorf("failed to execute command: %s: %v", cmd, err)
}
return net.ParseIP(strings.TrimSpace(string(out))), nil
}
func via(dest net.IP) (net.IP, error) {
cmd := fmt.Sprintf(`ip route get %s | grep via | cut -d" " -f3`, dest.String())
prefix := "ip"
if dest.To4() == nil {
prefix = "ip -6"
}
cmd := fmt.Sprintf(`%s route get %s | grep via | cut -d" " -f3`, prefix, dest.String())
cmdList := getCmdList(cmd)
out, err := exec.Command(execCmd, cmdList...).Output()
if err != nil {
return nil, fmt.Errorf("Failed to execute command: %s: %v", cmd, err)
return nil, fmt.Errorf("failed to execute command: %s: %v", cmd, err)
}
if string(out) == "" {
// assume the provided dest is the next hop
@@ -55,7 +63,7 @@ func localAddress(gw net.IP) (net.IP, error) {
}
}
}
return nil, fmt.Errorf("Unable to find local address")
return nil, fmt.Errorf("unable to find local address")
}
func addLoopback(name string, addr *net.IPNet) error {
@@ -66,30 +74,42 @@ func addLoopback(name string, addr *net.IPNet) error {
if len(label) > 15 {
label = label[:15]
}
cmd := fmt.Sprintf("ip address add %s/%d dev lo label %s", addr.IP.String(), prefixLen, label)
prefix := "ip"
if addr.IP.To4() == nil {
prefix = "ip -6"
}
cmd := fmt.Sprintf("%s address add %s/%d dev lo label %s", prefix, addr.IP.String(), prefixLen, label)
cmdList := getCmdList(cmd)
_, err := exec.Command(execCmd, cmdList...).Output()
if err != nil {
return fmt.Errorf("Failed to Add loopback command: %s: %v", cmd, err)
return fmt.Errorf("failed to Add loopback command: %s: %v", cmd, err)
}
return nil
}
func deleteLoopback(addr *net.IPNet) error {
prefix := "ip"
if addr.IP.To4() == nil {
prefix = "ip -6"
}
prefixLen, _ := addr.Mask.Size()
cmd := fmt.Sprintf("ip address delete %s/%d dev lo", addr.IP.String(), prefixLen)
cmd := fmt.Sprintf("%s address delete %s/%d dev lo", prefix, addr.IP.String(), prefixLen)
cmdList := getCmdList(cmd)
_, err := exec.Command(execCmd, cmdList...).Output()
if err != nil {
return fmt.Errorf("Failed to delete loopback command: %s: %v", cmd, err)
return fmt.Errorf("failed to delete loopback command: %s: %v", cmd, err)
}
return nil
}
func natRule(op string, vip, localAddr net.IP, protocol, lport, dport string) error {
prefix := "iptables"
if vip.To4() == nil {
prefix = "ip6tables"
}
cmd := fmt.Sprintf(
"iptables -t nat -%s PREROUTING -p %s -d %s --dport %s -j DNAT --to-destination %s:%s",
op, protocol, vip.String(), lport, localAddr.String(), dport,
"%s -t nat -%s PREROUTING -p %s -d %s --dport %s -j DNAT --to-destination %s:%s",
prefix, op, protocol, vip.String(), lport, localAddr.String(), dport,
)
cmdList := getCmdList(cmd)
_, err := exec.Command(execCmd, cmdList...).Output()

View File

@@ -12,9 +12,14 @@ import (
func TestGateway(t *testing.T) {
execCmd = os.Args[0]
os.Setenv("test_name", "test_gateway")
gw, err := gateway()
gw, err := gateway(4)
assert.Nil(t, err)
assert.Equal(t, "10.1.1.1", gw.String())
os.Setenv("test_name", "test_gateway_v6")
gw, err = gateway(6)
assert.Nil(t, err)
assert.Equal(t, "2001:dead:beef::1", gw.String())
}
func TestVia(t *testing.T) {
@@ -28,6 +33,11 @@ func TestVia(t *testing.T) {
ip, err = via(net.ParseIP("10.1.4.1"))
assert.Nil(t, err)
assert.Equal(t, "10.1.4.1", ip.String())
os.Setenv("test_name", "test_via_v6")
ip, err = via(net.ParseIP("2001:dead:beef::100"))
assert.Nil(t, err)
assert.Equal(t, "2001:dead:beef::1", ip.String())
}
func TestAddLoopback(t *testing.T) {
@@ -41,14 +51,23 @@ func TestAddLoopback(t *testing.T) {
_, ipnet, _ = net.ParseCIDR("1.1.1.1/32")
err = addLoopback("test_app", ipnet)
assert.NotNil(t, err)
os.Setenv("test_name", "test_add_v6")
_, ipnet, _ = net.ParseCIDR("2001:dead:beef:1001::100/64")
err = addLoopback("test_app", ipnet)
assert.Nil(t, err)
}
func TestMain(m *testing.M) {
switch os.Getenv("test_name") {
case "test_gateway":
fmt.Println("10.1.1.1")
case "test_gateway_v6":
fmt.Println("2001:dead:beef::1")
case "test_via":
fmt.Println("10.1.2.1")
case "test_via_v6":
fmt.Println("2001:dead:beef::1")
case "test_via_none":
break
case "test_add_fail":

BIN
gocast

Binary file not shown.

View File

@@ -72,11 +72,11 @@ func (s *Server) unregisterHandler(w http.ResponseWriter, r *http.Request) {
}
func (s *Server) infoHandler(w http.ResponseWriter, r *http.Request) {
peers, err := s.mon.GetInfo()
peer, err := s.mon.GetInfo()
if err != nil {
http.Error(w, fmt.Sprintf("Internal error getting peers: %v", err), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(peers)
json.NewEncoder(w).Encode(peer)
}