diff --git a/config/isis.go b/config/isis.go new file mode 100644 index 0000000000000000000000000000000000000000..c0c5de5a773dac856ea466e0244e56149b40f838 --- /dev/null +++ b/config/isis.go @@ -0,0 +1,70 @@ +package config + +import ( + "fmt" + + "github.com/bio-routing/bio-rd/protocols/isis/types" +) + +type ISISConfig struct { + NETs []NET + Interfaces []ISISInterfaceConfig + TrafficEngineeringRouterID [4]byte +} + +type ISISInterfaceConfig struct { + Name string + Passive bool + P2P bool + ISISLevel1Config *ISISLevelConfig + ISISLevel2Config *ISISLevelConfig +} + +type ISISLevelConfig struct { + HelloInterval uint16 + HoldTime uint16 + Metric uint32 + Priority uint8 +} + +// NET represents an ISO network entity title +type NET struct { + AFI byte + AreaID types.AreaID + SystemID types.SystemID + SEL byte +} + +func parseNET(addr []byte) (*NET, error) { + l := len(addr) + + if l < 8 { + return nil, fmt.Errorf("NET too short") + } + + if l > 20 { + return nil, fmt.Errorf("NET too long") + } + + areaID := []byte{} + + for i := 0; i < l-8; i++ { + areaID = append(areaID, addr[i+1]) + } + + systemID := types.SystemID{ + addr[l-7], + addr[l-6], + addr[l-5], + addr[l-4], + addr[l-3], + addr[l-2], + } + + return &NET{ + AFI: addr[0], + AreaID: areaID, + SystemID: systemID, + SEL: addr[l-1], + }, nil +} diff --git a/config/isis_test.go b/config/isis_test.go new file mode 100644 index 0000000000000000000000000000000000000000..602606495b3b1f4a60986113e880cbbcf1cd679a --- /dev/null +++ b/config/isis_test.go @@ -0,0 +1,67 @@ +package config + +import ( + "testing" + + "github.com/bio-routing/bio-rd/protocols/isis/types" + "github.com/stretchr/testify/assert" +) + +func TestParseNET(t *testing.T) { + tests := []struct { + name string + input []byte + wantFail bool + expected *NET + }{ + { + name: "Too short", + input: []byte{49, 1, 2, 3, 4, 5, 0}, + wantFail: true, + }, + { + name: "Too long", + input: []byte{0x49, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 1, 2, 3, 4, 5, 6, 0, 0}, + wantFail: true, + }, + { + name: "Max area ID length", + input: []byte{0x49, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 1, 2, 3, 4, 5, 6, 0}, + expected: &NET{ + AFI: 0x49, + AreaID: []byte{1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}, + SystemID: types.SystemID{1, 2, 3, 4, 5, 6}, + SEL: 0x00, + }, + }, + { + name: "No Area ID", + input: []byte{0x49, 1, 2, 3, 4, 5, 6, 0}, + expected: &NET{ + AFI: 0x49, + AreaID: []byte{}, + SystemID: types.SystemID{1, 2, 3, 4, 5, 6}, + SEL: 0x00, + }, + }, + } + + for _, test := range tests { + NET, err := parseNET(test.input) + if err != nil { + if test.wantFail { + continue + } + + t.Errorf("Unexpected error for test %q: %v", test.name, err) + continue + } + + if test.wantFail { + t.Errorf("Unexpected success for test %q", test.name) + } + + assert.Equalf(t, test.expected, NET, "Test: %q", test.name) + + } +} diff --git a/protocols/device/device.go b/protocols/device/device.go index 8939ca531a6530cb576aea4b93c25f7f14057327..d0eafe7549c3c90cede52c720f139ac81ac8c478 100644 --- a/protocols/device/device.go +++ b/protocols/device/device.go @@ -7,6 +7,16 @@ import ( bnet "github.com/bio-routing/bio-rd/net" ) +const ( + IfOperUnknown = 0 + IfOperNotPresent = 1 + IfOperDown = 2 + IfOperLowerLayerDown = 3 + IfOperTesting = 4 + IfOperDormant = 5 + IfOperUp = 6 +) + // Device represents a network device type Device struct { Name string diff --git a/protocols/device/server.go b/protocols/device/server.go index 00e29266830a87543505c8e0592d724afdbd9ba2..f01a82c6e7c33638134c2528474c8913fb2e0d96 100644 --- a/protocols/device/server.go +++ b/protocols/device/server.go @@ -6,6 +6,12 @@ import ( "github.com/pkg/errors" ) +// Updater is a device updater interface +type Updater interface { + Subscribe(Client, string) + Unsubscribe(Client, string) +} + // Server represents a device server type Server struct { devices map[uint64]*Device @@ -67,8 +73,8 @@ func (ds *Server) Subscribe(client Client, devName string) { client.DeviceUpdate(d) } - ds.clientsByDeviceMu.RLock() - defer ds.clientsByDeviceMu.RUnlock() + ds.clientsByDeviceMu.Lock() + defer ds.clientsByDeviceMu.Unlock() if _, ok := ds.clientsByDevice[devName]; !ok { ds.clientsByDevice[devName] = make([]Client, 0) @@ -77,6 +83,25 @@ func (ds *Server) Subscribe(client Client, devName string) { ds.clientsByDevice[devName] = append(ds.clientsByDevice[devName], client) } +// Unsubscribe unsubscribes a client +func (ds *Server) Unsubscribe(client Client, devName string) { + ds.clientsByDeviceMu.Lock() + defer ds.clientsByDeviceMu.Unlock() + + if _, ok := ds.clientsByDevice[devName]; !ok { + return + } + + for i := range ds.clientsByDevice[devName] { + if ds.clientsByDevice[devName][i] != client { + continue + } + + ds.clientsByDevice[devName] = append(ds.clientsByDevice[devName][:i], ds.clientsByDevice[devName][i+1:]...) + return + } +} + func (ds *Server) addDevice(d *Device) { ds.devicesMu.Lock() defer ds.devicesMu.Unlock() diff --git a/protocols/device/server_darwin.go b/protocols/device/server_darwin.go index 364843f7dd729f08a114d6e24e6b62a432024ec0..803b9a1a7420f87f09ef33443f3e57af49242ed3 100644 --- a/protocols/device/server_darwin.go +++ b/protocols/device/server_darwin.go @@ -1,14 +1,32 @@ package device -import "fmt" +import ( + "fmt" + + "github.com/pkg/errors" +) + +func (ds *Server) loadAdapter() error { + a, err := newOSAdapterLinux(ds) + if err != nil { + return errors.Wrap(err, "Unable to create linux adapter") + } + + ds.osAdapter = a + return nil +} type osAdapterDarwin struct { } -func newOSAdapterDarwin(srv *Server) (*osAdapterDarwin, error) { - return nil, nil +func newOSAdapterLinux(srv *Server) (*osAdapterDarwin, error) { + return nil, fmt.Errorf("Not implemented") } func (o *osAdapterDarwin) start() error { return fmt.Errorf("Not implemented") } + +func (o *osAdapterDarwin) init() error { + return fmt.Errorf("Not implemented") +} diff --git a/protocols/device/server_mock.go b/protocols/device/server_mock.go new file mode 100644 index 0000000000000000000000000000000000000000..19cc6e5048cbc5422d4176a6c45530673e340b0a --- /dev/null +++ b/protocols/device/server_mock.go @@ -0,0 +1,19 @@ +package device + +type MockServer struct { + Called bool + UnsubscribeCalled bool + C Client + Name string + UnsubscribeName string +} + +func (ms *MockServer) Subscribe(c Client, n string) { + ms.Called = true + ms.Name = n +} + +func (ms *MockServer) Unsubscribe(c Client, n string) { + ms.UnsubscribeCalled = true + ms.UnsubscribeName = n +} diff --git a/protocols/device/server_test.go b/protocols/device/server_test.go index 87a0f70e2bbaff47682b964e787a0865c560a08b..1e3e5b0070a35b825810f11bb2f000be86e021ac 100644 --- a/protocols/device/server_test.go +++ b/protocols/device/server_test.go @@ -81,6 +81,7 @@ func TestStop(t *testing.T) { type mockClient struct { deviceUpdateCalled uint + name string } func (m *mockClient) DeviceUpdate(d *Device) { @@ -117,3 +118,134 @@ func TestNotify(t *testing.T) { s.notify(101) assert.Equal(t, uint(2), mc.deviceUpdateCalled) } + +func TestUnsubscribe(t *testing.T) { + tests := []struct { + name string + ds *Server + unsubscribeDev string + unsubscribeClient int + expected *Server + }{ + { + name: "Remove single", + ds: &Server{ + clientsByDevice: map[string][]Client{ + "eth0": { + &mockClient{ + name: "foo", + }, + }, + }, + }, + unsubscribeDev: "eth0", + unsubscribeClient: 0, + expected: &Server{ + clientsByDevice: map[string][]Client{ + "eth0": {}, + }, + }, + }, + { + name: "Remove middle", + ds: &Server{ + clientsByDevice: map[string][]Client{ + "eth0": { + &mockClient{ + name: "foo", + }, + &mockClient{ + name: "bar", + }, + &mockClient{ + name: "baz", + }, + }, + }, + }, + unsubscribeDev: "eth0", + unsubscribeClient: 1, + expected: &Server{ + clientsByDevice: map[string][]Client{ + "eth0": { + &mockClient{ + name: "foo", + }, + &mockClient{ + name: "baz", + }, + }, + }, + }, + }, + { + name: "Remove first", + ds: &Server{ + clientsByDevice: map[string][]Client{ + "eth0": { + &mockClient{ + name: "foo", + }, + &mockClient{ + name: "bar", + }, + &mockClient{ + name: "baz", + }, + }, + }, + }, + unsubscribeDev: "eth0", + unsubscribeClient: 0, + expected: &Server{ + clientsByDevice: map[string][]Client{ + "eth0": { + &mockClient{ + name: "bar", + }, + &mockClient{ + name: "baz", + }, + }, + }, + }, + }, + { + name: "Remove last", + ds: &Server{ + clientsByDevice: map[string][]Client{ + "eth0": { + &mockClient{ + name: "foo", + }, + &mockClient{ + name: "bar", + }, + &mockClient{ + name: "baz", + }, + }, + }, + }, + unsubscribeDev: "eth0", + unsubscribeClient: 2, + expected: &Server{ + clientsByDevice: map[string][]Client{ + "eth0": { + &mockClient{ + name: "foo", + }, + &mockClient{ + name: "bar", + }, + }, + }, + }, + }, + } + + for _, test := range tests { + test.ds.Unsubscribe(test.ds.clientsByDevice[test.unsubscribeDev][test.unsubscribeClient], test.unsubscribeDev) + assert.Equal(t, test.expected, test.ds, test.name) + } +} diff --git a/protocols/isis/packet/isis.go b/protocols/isis/packet/isis.go index 8e1438c253f00310c3395c2cb94479dac6f154f8..b7edb627cb73dd9d6724ed981fc53d2c9ded51ef 100644 --- a/protocols/isis/packet/isis.go +++ b/protocols/isis/packet/isis.go @@ -21,6 +21,14 @@ const ( UP_STATE = 0 ) +var ( + AllL1ISS = [6]byte{0x01, 0x80, 0xC2, 0x00, 0x00, 0x14} + AllL2ISS = [6]byte{0x01, 0x80, 0xC2, 0x00, 0x00, 0x15} + AllP2PISS = [6]byte{0x09, 0x00, 0x2b, 0x00, 0x00, 0x05} + AllISS = [6]byte{0x09, 0x00, 0x2B, 0x00, 0x00, 0x05} + AllESS = [6]byte{0x09, 0x00, 0x2B, 0x00, 0x00, 0x04} +) + // ISISPacket represents an ISIS packet type ISISPacket struct { Header *ISISHeader diff --git a/protocols/isis/packet/tlv_protocols_supported.go b/protocols/isis/packet/tlv_protocols_supported.go index a9597e155d539245a2c0e7fc158bb81311ac8c45..648ee36a0e1589620fe9602e2d88476114771dfa 100644 --- a/protocols/isis/packet/tlv_protocols_supported.go +++ b/protocols/isis/packet/tlv_protocols_supported.go @@ -7,8 +7,16 @@ import ( "github.com/bio-routing/bio-rd/util/decode" ) -// ProtocolsSupportedTLVType is the type value of an protocols supported TLV -const ProtocolsSupportedTLVType = 129 +const ( + // ProtocolsSupportedTLVType is the type value of an protocols supported TLV + ProtocolsSupportedTLVType = 129 + + // NLPIDIPv4 is the Network Layer Protocol ID for IPv4 + NLPIDIPv4 = uint8(0xcc) + + // NLPIDIPv6 is the Network Layer Protocol ID for IPv6 + NLPIDIPv6 = uint8(0x8e) +) // ProtocolsSupportedTLV represents a protocols supported TLV type ProtocolsSupportedTLV struct { diff --git a/protocols/isis/server/device.go b/protocols/isis/server/device.go new file mode 100644 index 0000000000000000000000000000000000000000..1df586558224a7deca49aa3bd4692dadeef08181 --- /dev/null +++ b/protocols/isis/server/device.go @@ -0,0 +1,123 @@ +package server + +import ( + "fmt" + "sync" + + "github.com/bio-routing/bio-rd/config" + "github.com/bio-routing/bio-rd/protocols/device" + "github.com/bio-routing/bio-rd/protocols/isis/packet" + "github.com/pkg/errors" + + log "github.com/sirupsen/logrus" +) + +type dev struct { + name string + srv *Server + sys sys + up bool + passive bool + p2p bool + level2 *level + supportedProtocols []uint8 + phy *device.Device + done chan struct{} + wg sync.WaitGroup + helloMethod func() + receiverMethod func() +} + +type level struct { + HelloInterval uint16 + HoldTime uint16 + Metric uint32 + neighbors *neighbors +} + +func newDev(srv *Server, ifcfg *config.ISISInterfaceConfig) *dev { + d := &dev{ + name: ifcfg.Name, + srv: srv, + passive: ifcfg.Passive, + p2p: ifcfg.P2P, + supportedProtocols: []uint8{packet.NLPIDIPv4, packet.NLPIDIPv6}, + done: make(chan struct{}), + } + + d.helloMethod = d.helloRoutine + d.receiverMethod = d.receiverRoutine + + if ifcfg.ISISLevel2Config != nil { + d.level2 = &level{} + d.level2.HelloInterval = ifcfg.ISISLevel2Config.HelloInterval + d.level2.HoldTime = ifcfg.ISISLevel2Config.HoldTime + d.level2.Metric = ifcfg.ISISLevel2Config.Metric + d.level2.neighbors = newNeighbors() + } + + return d +} + +// DeviceUpdate receives interface status information and manages ISIS interface state +func (d *dev) DeviceUpdate(phy *device.Device) { + d.phy = phy + if d.phy.OperState == device.IfOperUp { + err := d.enable() + if err != nil { + log.Errorf("Unable to enable ISIS on %q: %v", d.name, err) + } + return + } + + err := d.disable() + if err != nil { + log.Errorf("Unable to disable ISIS on %q: %v", d.name, err) + return + } +} + +func (d *dev) enable() error { + err := d.sys.openPacketSocket() + if err != nil { + return fmt.Errorf("Failed to open packet socket: %v", err) + } + + err = d.sys.mcastJoin(packet.AllP2PISS) + if err != nil { + return fmt.Errorf("Failed to join multicast group: %v", err) + } + + d.done = make(chan struct{}) + + d.wg.Add(1) + go d.receiverMethod() + + d.wg.Add(1) + go d.helloMethod() + + log.Infof("ISIS: Interface %q is now up", d.name) + d.up = true + return nil +} + +func (d *dev) disable() error { + close(d.done) + + err := d.sys.closePacketSocket() + if err != nil { + return errors.Wrap(err, "Unable to close socket") + } + + d.wg.Wait() + d.up = false + return nil +} + +func (d *dev) receiverRoutine() { + // To be implemented +} + +func (d *dev) helloRoutine() { + // To be implemented +} diff --git a/protocols/isis/server/device_test.go b/protocols/isis/server/device_test.go new file mode 100644 index 0000000000000000000000000000000000000000..4b67c879f74d42623e46b5f76d0af49090cdf15d --- /dev/null +++ b/protocols/isis/server/device_test.go @@ -0,0 +1,141 @@ +package server + +import ( + "testing" + + "github.com/bio-routing/bio-rd/protocols/device" + "github.com/stretchr/testify/assert" +) + +func (d *dev) mockRecv() { + <-d.done + d.wg.Done() +} + +func (d *dev) mockHello() { + <-d.done + d.wg.Done() +} + +func TestEnableDisable(t *testing.T) { + tests := []struct { + name string + dev *dev + wantFail bool + }{ + { + name: "Failed open() for socket", + dev: &dev{ + sys: &mockSys{ + wantFailOpenPacketSocket: true, + }, + }, + wantFail: true, + }, + { + name: "Failed mcast join", + dev: &dev{ + sys: &mockSys{ + wantFailMcastJoin: true, + }, + }, + wantFail: true, + }, + { + name: "Success", + dev: &dev{ + sys: &mockSys{}, + }, + wantFail: false, + }, + } + + for _, test := range tests { + test.dev.receiverMethod = test.dev.mockRecv + test.dev.helloMethod = test.dev.mockHello + + err := test.dev.enable() + if err != nil { + if test.wantFail { + continue + } + + t.Errorf("Unexpected failure for test %q: %v", test.name, err) + continue + } + + if test.wantFail { + t.Errorf("Unexpected success for test %q", test.name) + } + + err = test.dev.disable() + if err != nil { + if test.wantFail { + continue + } + + t.Errorf("Unexpected failure for test %q: %v", test.name, err) + continue + } + + if test.wantFail { + t.Errorf("Unexpected success for test %q", test.name) + } + + assert.Equal(t, true, test.dev.sys.(*mockSys).closePacketSocketCalled) + } +} + +func TestDeviceUpdate(t *testing.T) { + tests := []struct { + name string + dev *dev + update *device.Device + expected bool + }{ + { + name: "Enable", + dev: &dev{ + up: false, + sys: &mockSys{}, + }, + update: &device.Device{ + OperState: device.IfOperUp, + }, + expected: true, + }, + { + name: "Disable #1", + dev: &dev{ + done: make(chan struct{}), + up: true, + sys: &mockSys{}, + }, + update: &device.Device{ + OperState: device.IfOperLowerLayerDown, + }, + expected: false, + }, + { + name: "Disable #2", + dev: &dev{ + done: make(chan struct{}), + up: true, + sys: &mockSys{}, + }, + update: &device.Device{ + OperState: device.IfOperDown, + }, + expected: false, + }, + } + + for _, test := range tests { + test.dev.receiverMethod = test.dev.mockRecv + test.dev.helloMethod = test.dev.mockHello + + test.dev.DeviceUpdate(test.update) + + assert.Equal(t, test.expected, test.dev.up, test.name) + } +} diff --git a/protocols/isis/server/devices.go b/protocols/isis/server/devices.go new file mode 100644 index 0000000000000000000000000000000000000000..c0ec70dcc6d365f902b115e5907ba17ec6f53665 --- /dev/null +++ b/protocols/isis/server/devices.go @@ -0,0 +1,55 @@ +package server + +import ( + "fmt" + "sync" + + "github.com/bio-routing/bio-rd/config" + "github.com/pkg/errors" +) + +type devices struct { + srv *Server + db map[string]*dev + dbMu sync.RWMutex +} + +func newDevices(srv *Server) *devices { + return &devices{ + srv: srv, + db: make(map[string]*dev), + } +} + +func (db *devices) addDevice(ifcfg *config.ISISInterfaceConfig) error { + db.dbMu.Lock() + defer db.dbMu.Unlock() + + if _, ok := db.db[ifcfg.Name]; ok { + return fmt.Errorf("Interface exists already") + } + + d := newDev(db.srv, ifcfg) + db.db[ifcfg.Name] = d + + db.srv.ds.Subscribe(d, d.name) + return nil +} + +func (db *devices) removeDevice(name string) error { + db.dbMu.Lock() + defer db.dbMu.Unlock() + + if _, ok := db.db[name]; !ok { + return fmt.Errorf("Interface not found") + } + + db.srv.ds.Unsubscribe(db.db[name], name) + err := db.db[name].disable() + if err != nil { + return errors.Wrap(err, "Unable to disable interface") + } + + delete(db.db, name) + return nil +} diff --git a/protocols/isis/server/devices_test.go b/protocols/isis/server/devices_test.go new file mode 100644 index 0000000000000000000000000000000000000000..0aaec1dabb680bbbf2df43f6b8f715a5f6e0b0f6 --- /dev/null +++ b/protocols/isis/server/devices_test.go @@ -0,0 +1,250 @@ +package server + +import ( + "testing" + + "github.com/bio-routing/bio-rd/config" + "github.com/bio-routing/bio-rd/protocols/device" + "github.com/stretchr/testify/assert" +) + +func TestRemoveDevice(t *testing.T) { + tests := []struct { + name string + db *devices + removeName string + wantFail bool + expected *devices + wantUnregister bool + }{ + { + name: "Remove existing", + db: &devices{ + srv: &Server{ + ds: &device.MockServer{}, + }, + db: map[string]*dev{ + "foobar": { + done: make(chan struct{}), + sys: &mockSys{}, + name: "foobar", + }, + }, + }, + removeName: "foobar", + expected: &devices{ + srv: &Server{ + ds: &device.MockServer{ + UnsubscribeCalled: true, + UnsubscribeName: "foobar", + }, + }, + db: map[string]*dev{}, + }, + wantUnregister: true, + }, + { + name: "Remove non-existing", + db: &devices{ + srv: &Server{ + ds: &device.MockServer{}, + }, + db: map[string]*dev{ + "foobar": { + done: make(chan struct{}), + sys: &mockSys{}, + name: "foobar", + }, + }, + }, + removeName: "baz", + expected: &devices{ + srv: &Server{ + ds: &device.MockServer{}, + }, + db: map[string]*dev{ + "foobar": { + done: make(chan struct{}), + sys: &mockSys{}, + name: "foobar", + }, + }, + }, + wantUnregister: false, + wantFail: true, + }, + { + name: "Remove existing - disable fails", + db: &devices{ + srv: &Server{ + ds: &device.MockServer{}, + }, + db: map[string]*dev{ + "foobar": { + done: make(chan struct{}), + sys: &mockSys{ + wantFailClosedPacketSocket: true, + }, + name: "foobar", + }, + }, + }, + removeName: "foobar", + expected: &devices{ + srv: &Server{ + ds: &device.MockServer{ + UnsubscribeCalled: true, + UnsubscribeName: "foobar", + }, + }, + db: map[string]*dev{}, + }, + wantUnregister: true, + wantFail: true, + }, + } + + for _, test := range tests { + err := test.db.removeDevice(test.removeName) + + assert.Equal(t, test.wantUnregister, test.db.srv.ds.(*device.MockServer).UnsubscribeCalled, test.name) + + if err != nil { + if test.wantFail { + continue + } + + t.Errorf("Unexpected failure for test %q: %v", test.name, err) + continue + } + + if test.wantFail { + t.Errorf("Unexpected success for test %q", test.name) + } + + // Ignore some attributes + for i := range test.db.db { + test.db.db[i].srv = nil + test.db.db[i].helloMethod = nil + test.db.db[i].receiverMethod = nil + test.db.db[i].done = nil + } + + assert.Equal(t, test.expected, test.db, test.name) + } +} + +func TestDeviceAddDevice(t *testing.T) { + tests := []struct { + name string + db *devices + addIfCfg *config.ISISInterfaceConfig + wantFail bool + expected *devices + wantRegister bool + }{ + { + name: "Test #1", + db: &devices{ + srv: &Server{ + ds: &device.MockServer{}, + }, + db: map[string]*dev{ + "foobar": { + name: "foobar", + }, + }, + }, + addIfCfg: &config.ISISInterfaceConfig{ + Name: "baz", + Passive: true, + ISISLevel2Config: &config.ISISLevelConfig{ + HelloInterval: 5, + }, + }, + expected: &devices{ + srv: &Server{ + ds: &device.MockServer{ + Called: true, + Name: "baz", + }, + }, + db: map[string]*dev{ + "foobar": { + name: "foobar", + }, + "baz": { + name: "baz", + passive: true, + supportedProtocols: []uint8{0xcc, 0x8e}, + level2: &level{ + HelloInterval: 5, + }, + }, + }, + }, + wantRegister: true, + }, + { + name: "Test #2", + db: &devices{ + srv: &Server{ + ds: &device.MockServer{}, + }, + db: map[string]*dev{ + "foobar": { + name: "foobar", + }, + }, + }, + addIfCfg: &config.ISISInterfaceConfig{ + Name: "foobar", + Passive: true, + }, + expected: &devices{ + srv: &Server{ + ds: &device.MockServer{ + Called: true, + Name: "baz", + }, + }, + db: map[string]*dev{ + "foobar": { + name: "foobar", + }, + }, + }, + wantRegister: false, + wantFail: true, + }, + } + + for _, test := range tests { + err := test.db.addDevice(test.addIfCfg) + + assert.Equal(t, test.wantRegister, test.db.srv.ds.(*device.MockServer).Called, test.name) + + if err != nil { + if test.wantFail { + continue + } + + t.Errorf("Unexpected failure for test %q: %v", test.name, err) + continue + } + + if test.wantFail { + t.Errorf("Unexpected success for test %q", test.name) + } + + // Ignore some attributes + for i := range test.db.db { + test.db.db[i].srv = nil + test.db.db[i].helloMethod = nil + test.db.db[i].receiverMethod = nil + test.db.db[i].done = nil + } + + assert.Equal(t, test.expected, test.db, test.name) + } +} diff --git a/protocols/isis/server/lsdb.go b/protocols/isis/server/lsdb.go new file mode 100644 index 0000000000000000000000000000000000000000..0bf96060ae54b15e9eaceb4d6793196eb859bc86 --- /dev/null +++ b/protocols/isis/server/lsdb.go @@ -0,0 +1,71 @@ +package server + +import ( + "sync" + + "github.com/bio-routing/bio-rd/protocols/isis/packet" + btime "github.com/bio-routing/bio-rd/util/time" +) + +type lsdb struct { + srv *Server + lsps map[packet.LSPID]*lsdbEntry + lspsMu sync.RWMutex + done chan struct{} + wg sync.WaitGroup +} + +type lsdbEntry struct { + lspdu *packet.LSPDU + srmFlags map[*dev]struct{} + ssnFlags map[*dev]struct{} +} + +func newLSDB(s *Server) *lsdb { + return &lsdb{ + srv: s, + done: make(chan struct{}), + } +} + +func (l *lsdb) dispose() { + l.stop() + l.srv = nil +} + +func (l *lsdb) start(t btime.Ticker) { + l.wg.Add(1) + go l.decrementRemainingLifetimesRoutine(t) +} + +func (l *lsdb) stop() { + close(l.done) + l.wg.Wait() +} + +func (l *lsdb) decrementRemainingLifetimesRoutine(t btime.Ticker) { + defer l.wg.Done() + + for { + select { + case <-t.C(): + l.decrementRemainingLifetimes() + case <-l.done: + return + } + } +} + +func (l *lsdb) decrementRemainingLifetimes() { + l.lspsMu.Lock() + defer l.lspsMu.Unlock() + + for lspid, lspdbEntry := range l.lsps { + if lspdbEntry.lspdu.RemainingLifetime <= 1 { + delete(l.lsps, lspid) + continue + } + + lspdbEntry.lspdu.RemainingLifetime-- + } +} diff --git a/protocols/isis/server/lsdb_test.go b/protocols/isis/server/lsdb_test.go new file mode 100644 index 0000000000000000000000000000000000000000..6c6c27aed5d2e5566a54e7b04904542f905b1b59 --- /dev/null +++ b/protocols/isis/server/lsdb_test.go @@ -0,0 +1,135 @@ +package server + +import ( + "testing" + + "github.com/bio-routing/bio-rd/protocols/isis/packet" + "github.com/bio-routing/bio-rd/protocols/isis/types" + btime "github.com/bio-routing/bio-rd/util/time" + "github.com/stretchr/testify/assert" +) + +func TestLSDPDispose(t *testing.T) { + l := newLSDB(&Server{}) + l.dispose() + + if l.srv != nil { + t.Errorf("srv reference not cleared") + } +} + +func TestDecrementRemainingLifetimes(t *testing.T) { + tests := []struct { + name string + lsdb *lsdb + expected *lsdb + }{ + { + name: "Test #1", + lsdb: &lsdb{ + lsps: map[packet.LSPID]*lsdbEntry{ + { + SystemID: types.SystemID{10, 20, 30, 40, 50, 60}, + PseudonodeID: 0x00, + LSPNumber: 1, + }: { + lspdu: &packet.LSPDU{ + RemainingLifetime: 5, + }, + }, + { + SystemID: types.SystemID{11, 22, 33, 44, 55, 66}, + PseudonodeID: 0x00, + LSPNumber: 1, + }: { + lspdu: &packet.LSPDU{ + RemainingLifetime: 1, + }, + }, + }, + }, + expected: &lsdb{ + lsps: map[packet.LSPID]*lsdbEntry{ + { + SystemID: types.SystemID{10, 20, 30, 40, 50, 60}, + PseudonodeID: 0x00, + LSPNumber: 1, + }: { + lspdu: &packet.LSPDU{ + RemainingLifetime: 4, + }, + }, + }, + }, + }, + } + + for _, test := range tests { + test.lsdb.decrementRemainingLifetimes() + assert.Equal(t, test.expected, test.lsdb) + } +} + +func TestStartStop(t *testing.T) { + db := &lsdb{ + done: make(chan struct{}), + lsps: map[packet.LSPID]*lsdbEntry{ + { + SystemID: types.SystemID{10, 20, 30, 40, 50, 60}, + PseudonodeID: 0x00, + LSPNumber: 1, + }: { + lspdu: &packet.LSPDU{ + RemainingLifetime: 5, + }, + }, + { + SystemID: types.SystemID{11, 22, 33, 44, 55, 66}, + PseudonodeID: 0x00, + LSPNumber: 1, + }: { + lspdu: &packet.LSPDU{ + RemainingLifetime: 1, + }, + }, + }, + } + expected := &lsdb{ + done: make(chan struct{}), + lsps: map[packet.LSPID]*lsdbEntry{ + { + SystemID: types.SystemID{10, 20, 30, 40, 50, 60}, + PseudonodeID: 0x00, + LSPNumber: 1, + }: { + lspdu: &packet.LSPDU{ + RemainingLifetime: 5, + }, + }, + { + SystemID: types.SystemID{11, 22, 33, 44, 55, 66}, + PseudonodeID: 0x00, + LSPNumber: 1, + }: { + lspdu: &packet.LSPDU{ + RemainingLifetime: 1, + }, + }, + }, + } + ticker := btime.NewMockTicker() + db.start(ticker) + + expected.decrementRemainingLifetimes() + expected.decrementRemainingLifetimes() + expected.decrementRemainingLifetimes() + expected.decrementRemainingLifetimes() + + ticker.Tick() + ticker.Tick() + ticker.Tick() + ticker.Tick() + + db.stop() + assert.Equal(t, db.lsps, expected.lsps) +} diff --git a/protocols/isis/server/neighbor.go b/protocols/isis/server/neighbor.go new file mode 100644 index 0000000000000000000000000000000000000000..62982b32d8938c80e4b978888fb80efb73a169ff --- /dev/null +++ b/protocols/isis/server/neighbor.go @@ -0,0 +1,13 @@ +package server + +import "github.com/bio-routing/bio-rd/protocols/isis/types" + +type neighbor struct { + systemID types.SystemID + dev *dev + holdingTime uint16 + localCircuitID uint8 + extendedLocalCircuitID uint32 + ipInterfaceAddresses []uint32 + //fsm *FSM +} diff --git a/protocols/isis/server/neighbors.go b/protocols/isis/server/neighbors.go new file mode 100644 index 0000000000000000000000000000000000000000..fcfb1549d2791cf744629f3ca01730db29ff1874 --- /dev/null +++ b/protocols/isis/server/neighbors.go @@ -0,0 +1,16 @@ +package server + +import ( + "sync" + + "github.com/bio-routing/bio-rd/protocols/isis/types" +) + +type neighbors struct { + db map[types.MACAddress]*neighbor + dbMu sync.RWMutex +} + +func newNeighbors() *neighbors { + return nil +} diff --git a/protocols/isis/server/server.go b/protocols/isis/server/server.go new file mode 100644 index 0000000000000000000000000000000000000000..fbac57bec5300f634f09a40fe49639b3e9948f30 --- /dev/null +++ b/protocols/isis/server/server.go @@ -0,0 +1,51 @@ +package server + +import ( + "time" + + "github.com/bio-routing/bio-rd/config" + "github.com/bio-routing/bio-rd/protocols/device" + btime "github.com/bio-routing/bio-rd/util/time" +) + +//Server represents an ISIS server +type Server struct { + config *config.ISISConfig + sequenceNumber uint32 + devices *devices + lsdb *lsdb + stop chan struct{} + ds device.Updater +} + +func New(cfg *config.ISISConfig, ds device.Updater) *Server { + s := &Server{ + config: cfg, + ds: ds, + sequenceNumber: 1, + stop: make(chan struct{}), + } + + s.devices = newDevices(s) + s.lsdb = newLSDB(s) + return s +} + +func (s *Server) start() { + s.lsdb.start(btime.NewBIOTicker(time.Second)) +} + +func (s *Server) dispose() { + s.lsdb.dispose() + s.lsdb = nil +} + +// AddInterface adds an interface to the ISIS Server +func (s *Server) AddInterface(ifcfg *config.ISISInterfaceConfig) { + s.devices.addDevice(ifcfg) +} + +// RemoveInterface removes an interface from the ISIS Server +func (s *Server) RemoveInterface(name string) { + s.devices.removeDevice(name) +} diff --git a/protocols/isis/server/server_test.go b/protocols/isis/server/server_test.go new file mode 100644 index 0000000000000000000000000000000000000000..abb4e431abd516750a5a1e5e2b77073c236b8f9e --- /dev/null +++ b/protocols/isis/server/server_test.go @@ -0,0 +1 @@ +package server diff --git a/protocols/isis/server/sys.go b/protocols/isis/server/sys.go new file mode 100644 index 0000000000000000000000000000000000000000..400542cf87a0f0efec827e1f711e2dc2ea482bb5 --- /dev/null +++ b/protocols/isis/server/sys.go @@ -0,0 +1,71 @@ +package server + +import ( + "fmt" + + "github.com/bio-routing/bio-rd/protocols/device" + "github.com/bio-routing/bio-rd/protocols/isis/types" +) + +type sys interface { + openPacketSocket() error + closePacketSocket() error + mcastJoin(addr [6]byte) error + sendPacket(pkt []byte, dst [6]byte) error + recvPacket() (pkt []byte, src types.MACAddress, err error) +} + +type bioSys struct { + socket int + device *device.Device +} + +type mockSys struct { + wantFailOpenPacketSocket bool + wantFailClosedPacketSocket bool + wantFailMcastJoin bool + wantFailSendPacket bool + wantFailRecvPacket bool + closePacketSocketCalled bool +} + +func (m *mockSys) openPacketSocket() error { + if m.wantFailOpenPacketSocket { + return fmt.Errorf("Fail") + } + + return nil +} + +func (m *mockSys) closePacketSocket() error { + m.closePacketSocketCalled = true + if m.wantFailClosedPacketSocket { + return fmt.Errorf("Fail") + } + + return nil +} + +func (m *mockSys) mcastJoin(addr [6]byte) error { + if m.wantFailMcastJoin { + return fmt.Errorf("Fail") + } + + return nil +} + +func (m *mockSys) sendPacket(pkt []byte, dst [6]byte) error { + if m.wantFailSendPacket { + return fmt.Errorf("Fail") + } + + return nil +} + +func (m *mockSys) recvPacket() (pkt []byte, src types.MACAddress, err error) { + if m.wantFailRecvPacket { + return nil, [6]byte{}, fmt.Errorf("Fail") + } + + return []byte{1, 2, 3}, [6]byte{10, 20, 30, 40, 50, 60}, nil +} diff --git a/protocols/isis/server/sys_darwin.go b/protocols/isis/server/sys_darwin.go new file mode 100644 index 0000000000000000000000000000000000000000..eff533dd801c3ba378b7614c0bd3fc882a503dd1 --- /dev/null +++ b/protocols/isis/server/sys_darwin.go @@ -0,0 +1,27 @@ +package server + +import ( + "fmt" + + "github.com/bio-routing/bio-rd/protocols/isis/types" +) + +func (b *bioSys) openPacketSocket() error { + return fmt.Errorf("Unsupported platform") +} + +func (b *bioSys) closePacketSocket() error { + return fmt.Errorf("Unsupported platform") +} + +func (b *bioSys) mcastJoin(addr [6]byte) error { + return fmt.Errorf("Unsupported platform") +} + +func (b *bioSys) sendPacket(pkt []byte, dst [6]byte) error { + return fmt.Errorf("Unsupported platform") +} + +func (b *bioSys) recvPacket() (pkt []byte, src types.MACAddress, err error) { + return nil, types.MACAddress{}, fmt.Errorf("Unsupported platform") +} diff --git a/protocols/isis/server/sys_linux.go b/protocols/isis/server/sys_linux.go new file mode 100644 index 0000000000000000000000000000000000000000..bfe690cf6e036c459c324da9dbd74501f3027445 --- /dev/null +++ b/protocols/isis/server/sys_linux.go @@ -0,0 +1,78 @@ +package server + +import ( + "fmt" + "syscall" + + "github.com/bio-routing/bio-rd/protocols/isis/types" + "github.com/bio-routing/bio-rd/syscallwrappers" +) + +func (b *bioSys) openPacketSocket() error { + socket, err := syscall.Socket(syscall.AF_PACKET, syscall.SOCK_DGRAM, syscall.ETH_P_ALL) + if err != nil { + return fmt.Errorf("socket() failed: %v", err) + } + b.socket = socket + + if syscallwrappers.SetBPFFilter(b.socket) != 0 { + return fmt.Errorf("Unable to set BPF filter") + } + + if syscallwrappers.BindToInterface(b.socket, int(b.device.Index)) != 0 { + return fmt.Errorf("Unable to bind to interface") + } + + return nil +} + +func (b *bioSys) closePacketSocket() error { + return syscall.Close(b.socket) +} + +func (b *bioSys) mcastJoin(addr [6]byte) error { + if syscallwrappers.JoinISISMcast(b.socket, int(b.device.Index)) != 0 { + return fmt.Errorf("setsockopt failed") + } + + return nil +} + +func (b *bioSys) recvPacket() (pkt []byte, src types.MACAddress, err error) { + buf := make([]byte, 1500) + nBytes, from, err := syscall.Recvfrom(b.socket, buf, 0) + if err != nil { + return nil, types.MACAddress{}, fmt.Errorf("recvfrom failed: %v", err) + } + + ll := from.(*syscall.SockaddrLinklayer) + copy(src[:], ll.Addr[:6]) + + return buf[:nBytes], src, nil +} + +func (b *bioSys) sendPacket(pkt []byte, dst [6]byte) error { + ll := syscall.SockaddrLinklayer{ + Ifindex: int(b.device.Index), + Halen: 6, // MAC address length + } + + for i := uint8(0); i < ll.Halen; i++ { + ll.Addr[i] = dst[i] + } + + newPkt := []byte{ + 0xfe, 0xfe, 0x03, + } + + newPkt = append(newPkt, pkt...) + + ll.Protocol = uint16(len(newPkt)) + + err := syscall.Sendto(b.socket, newPkt, 0, &ll) + if err != nil { + return fmt.Errorf("sendto failed: %v", err) + } + + return nil +} diff --git a/syscallwrappers/syscalls_linux.go b/syscallwrappers/syscalls_linux.go new file mode 100644 index 0000000000000000000000000000000000000000..9649e2c3b75ce7e623bf7ce0e827e235f8d9c00b --- /dev/null +++ b/syscallwrappers/syscalls_linux.go @@ -0,0 +1,85 @@ +package syscallwrappers + +/* +#cgo CFLAGS: -I/usr/include +#cgo LDFLAGS: -L/usr/lib +#include <sys/types.h> +#include <sys/socket.h> +#include <linux/if_packet.h> +#include <linux/filter.h> +#include <net/ethernet.h> +#include <string.h> +#include <arpa/inet.h> +int setsockopt(int sockfd, int level, int optname, const void *optval, socklen_t optlen); +uint8_t ALL_L1_ISS[6] = {0x01, 0x80, 0xC2, 0x00, 0x00, 0x14}; +uint8_t ALL_L2_ISS[6] = {0x01, 0x80, 0xC2, 0x00, 0x00, 0x15}; +uint8_t ALL_P2P_ISS[6] = {0x09, 0x00, 0x2b, 0x00, 0x00, 0x5b}; +uint8_t ALL_ISS[6] = {0x09, 0x00, 0x2B, 0x00, 0x00, 0x05}; +uint8_t ALL_ESS[6] = {0x09, 0x00, 0x2B, 0x00, 0x00, 0x04}; +static struct sock_filter isisfilter[] = { + //{ 0x28, 0, 0, 0x0000000c }, { 0x25, 5, 0, 0x000005dc }, + { 0x28, 0, 0, 0x0000000e - 14 }, { 0x15, 0, 3, 0x0000fefe }, + { 0x30, 0, 0, 0x00000011 - 14 }, { 0x15, 0, 1, 0x00000083 }, + { 0x6, 0, 0, 0x00040000 }, { 0x6, 0, 0, 0x00000000 }, +}; +static struct sock_fprog bpf = { + .len = 6, + .filter = isisfilter, +}; +int reg_bpf(int fd) { + return setsockopt(fd, SOL_SOCKET, SO_ATTACH_FILTER, &bpf, sizeof(bpf)); +} +int bind_to_interface(int fd, int ifindex) { + struct sockaddr_ll s_addr; + memset(&s_addr, 0, sizeof(struct sockaddr_ll)); + s_addr.sll_family = AF_PACKET; + s_addr.sll_protocol = htons(ETH_P_ALL); + s_addr.sll_ifindex = ifindex; + return bind(fd, (struct sockaddr *)(&s_addr), sizeof(struct sockaddr_ll)); +} +int isis_multicast_join(int fd, int registerto, int ifindex) +{ + struct packet_mreq mreq; + memset(&mreq, 0, sizeof(mreq)); + mreq.mr_ifindex = ifindex; + if (registerto) { + mreq.mr_type = PACKET_MR_MULTICAST; + mreq.mr_alen = ETH_ALEN; + if (registerto == 1) + memcpy(&mreq.mr_address, ALL_L1_ISS, ETH_ALEN); + else if (registerto == 2) + memcpy(&mreq.mr_address, ALL_L2_ISS, ETH_ALEN); + else if (registerto == 3) + memcpy(&mreq.mr_address, ALL_ISS, ETH_ALEN); + else if (registerto == 4) + memcpy(&mreq.mr_address, ALL_P2P_ISS, ETH_ALEN); + else + memcpy(&mreq.mr_address, ALL_ESS, ETH_ALEN); + } else { + mreq.mr_type = PACKET_MR_ALLMULTI; + } + return setsockopt(fd, SOL_PACKET, PACKET_ADD_MEMBERSHIP, &mreq, sizeof(struct packet_mreq)); +} +*/ +import "C" + +import ( + "unsafe" +) + +func SetBPFFilter(sockfd int) int { + return int(C.reg_bpf(C.int(sockfd))) +} + +func SetSockOpt(sockfd int, level int, optName int, optVal uintptr, optLen int) int { + ptr := unsafe.Pointer(optVal) + return int(C.setsockopt(C.int(sockfd), C.int(level), C.int(optName), ptr, C.uint(optLen))) +} + +func JoinISISMcast(sockfd int, ifIndex int) int { + return int(C.isis_multicast_join(C.int(sockfd), 4, C.int(ifIndex))) +} + +func BindToInterface(sockfd int, ifIndex int) int { + return int(C.bind_to_interface(C.int(sockfd), C.int(ifIndex))) +} diff --git a/util/time/ticker.go b/util/time/ticker.go new file mode 100644 index 0000000000000000000000000000000000000000..9bfb6cda97f3d7cbe3dfc65410f06d408ad4db78 --- /dev/null +++ b/util/time/ticker.go @@ -0,0 +1,64 @@ +package time + +import ( + gotime "time" +) + +// Ticker is a ticker interface that allows mocking tickers +type Ticker interface { + C() <-chan gotime.Time + Stop() +} + +// BIOTicker is a wrapper for time.Ticker +type BIOTicker struct { + t *gotime.Ticker + ch <-chan gotime.Time +} + +// NewBIOTicker creates a new BIO ticker +func NewBIOTicker(interval gotime.Duration) *BIOTicker { + bt := &BIOTicker{ + t: gotime.NewTicker(interval), + } + + bt.ch = bt.t.C + return bt +} + +// C returns the channel +func (bt *BIOTicker) C() <-chan gotime.Time { + return bt.ch +} + +// Stop stops the ticker +func (bt *BIOTicker) Stop() { + bt.t.Stop() +} + +// MockTicker os a mocked ticker +type MockTicker struct { + ch chan gotime.Time +} + +// NewMockTicker creates a new mock ticker +func NewMockTicker() *MockTicker { + return &MockTicker{ + ch: make(chan gotime.Time), + } +} + +// C gets the channel of the ticker +func (m *MockTicker) C() <-chan gotime.Time { + return m.ch +} + +// Stop is here to fulfill an interface +func (m *MockTicker) Stop() { + +} + +// Tick lets the mock ticker tick +func (m *MockTicker) Tick() { + m.ch <- gotime.Now() +}