diff --git a/routingtable/filter/actions/accept_action.go b/routingtable/filter/actions/accept_action.go
new file mode 100644
index 0000000000000000000000000000000000000000..6c0cc2acbfa3d75603ff33cc10025bdad09b3a23
--- /dev/null
+++ b/routingtable/filter/actions/accept_action.go
@@ -0,0 +1,13 @@
+package actions
+
+import (
+	"github.com/bio-routing/bio-rd/net"
+	"github.com/bio-routing/bio-rd/route"
+)
+
+type AcceptAction struct {
+}
+
+func (*AcceptAction) Do(p net.Prefix, pa *route.Path) (modPath *route.Path, reject bool) {
+	return pa, false
+}
diff --git a/routingtable/filter/actions/reject_action.go b/routingtable/filter/actions/reject_action.go
new file mode 100644
index 0000000000000000000000000000000000000000..d740139626b99395db71a3b2e7c2cea1d04ebacf
--- /dev/null
+++ b/routingtable/filter/actions/reject_action.go
@@ -0,0 +1,13 @@
+package actions
+
+import (
+	"github.com/bio-routing/bio-rd/net"
+	"github.com/bio-routing/bio-rd/route"
+)
+
+type RejectAction struct {
+}
+
+func (*RejectAction) Do(p net.Prefix, pa *route.Path) (modPath *route.Path, reject bool) {
+	return pa, true
+}
diff --git a/routingtable/filter/filter.go b/routingtable/filter/filter.go
new file mode 100644
index 0000000000000000000000000000000000000000..f2b46997bd2d02a0c7e03d5bc0e4faccf180c9e7
--- /dev/null
+++ b/routingtable/filter/filter.go
@@ -0,0 +1,64 @@
+package filter
+
+import (
+	"github.com/bio-routing/bio-rd/net"
+	"github.com/bio-routing/bio-rd/route"
+	"github.com/bio-routing/bio-rd/routingtable"
+)
+
+type Filter struct {
+	routingtable.ClientManager
+	terms []*Term
+}
+
+func NewFilter(terms []*Term) *Filter {
+	f := &Filter{
+		terms: terms,
+	}
+	f.ClientManager = routingtable.NewClientManager(f)
+
+	return f
+}
+
+func (f *Filter) AddPath(p net.Prefix, pa *route.Path) error {
+	pa, rejected := f.processTerms(p, pa)
+	if rejected {
+		return nil
+	}
+
+	for _, c := range f.Clients() {
+		c.AddPath(p, pa)
+	}
+
+	return nil
+}
+
+func (f *Filter) RemovePath(p net.Prefix, pa *route.Path) bool {
+	pa, rejected := f.processTerms(p, pa)
+	if rejected {
+		return false
+	}
+
+	for _, c := range f.Clients() {
+		c.RemovePath(p, pa)
+	}
+
+	return true
+}
+
+func (f *Filter) UpdateNewClient(c routingtable.RouteTableClient) error {
+	return nil
+}
+
+func (f *Filter) processTerms(p net.Prefix, pa *route.Path) (modPath *route.Path, reject bool) {
+	modPath = pa
+
+	for _, t := range f.terms {
+		modPath, reject = t.Process(p, modPath)
+		if reject {
+			return modPath, true
+		}
+	}
+
+	return modPath, false
+}
diff --git a/routingtable/filter/filter_test.go b/routingtable/filter/filter_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..8b33f125e0824a1e44b5dad7f4b8daf9ec323247
--- /dev/null
+++ b/routingtable/filter/filter_test.go
@@ -0,0 +1,187 @@
+package filter
+
+import (
+	"testing"
+
+	"github.com/bio-routing/bio-rd/routingtable/filter/actions"
+	"github.com/stretchr/testify/assert"
+
+	"github.com/bio-routing/bio-rd/net"
+	"github.com/bio-routing/bio-rd/route"
+	"github.com/bio-routing/bio-rd/routingtable"
+)
+
+type clientMock struct {
+	routingtable.ClientManager
+	addPathCalled    bool
+	removePathCalled bool
+	path             *route.Path
+}
+
+func (m *clientMock) AddPath(p net.Prefix, pa *route.Path) error {
+	m.path = pa
+	m.addPathCalled = true
+	return nil
+}
+
+func (m *clientMock) RemovePath(p net.Prefix, pa *route.Path) bool {
+	m.path = pa
+	m.removePathCalled = true
+	return false
+}
+
+func (m *clientMock) UpdateNewClient(c routingtable.RouteTableClient) error {
+	return nil
+}
+
+func newClientMock() *clientMock {
+	m := &clientMock{}
+	m.ClientManager = routingtable.NewClientManager(m)
+	return m
+}
+
+func TestAddPath(t *testing.T) {
+	tests := []struct {
+		name           string
+		prefix         net.Prefix
+		path           *route.Path
+		term           *Term
+		exptectCalled  bool
+		expectModified bool
+	}{
+		{
+			name:   "accept",
+			prefix: net.NewPfx(0, 0),
+			path:   &route.Path{},
+			term: &Term{
+				then: []Then{
+					&actions.AcceptAction{},
+				},
+			},
+			exptectCalled:  true,
+			expectModified: false,
+		},
+		{
+			name:   "reject",
+			prefix: net.NewPfx(0, 0),
+			path:   &route.Path{},
+			term: &Term{
+				then: []Then{
+					&actions.RejectAction{},
+				},
+			},
+			exptectCalled:  false,
+			expectModified: false,
+		},
+		{
+			name:   "modified",
+			prefix: net.NewPfx(0, 0),
+			path:   &route.Path{},
+			term: &Term{
+				then: []Then{
+					&mockAction{},
+					&actions.AcceptAction{},
+				},
+			},
+			exptectCalled:  true,
+			expectModified: true,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(te *testing.T) {
+			m := newClientMock()
+
+			f := NewFilter([]*Term{test.term})
+			f.Register(m)
+
+			f.AddPath(test.prefix, test.path)
+			assert.Equal(te, test.exptectCalled, m.addPathCalled, "called")
+
+			if !test.exptectCalled {
+				return
+			}
+
+			if m.path != test.path && !test.expectModified {
+				te.Fatal("expected path to be not modified but was")
+			}
+
+			if m.path == test.path && test.expectModified {
+				te.Fatal("expected path to be modified but was same reference")
+			}
+		})
+	}
+}
+
+func TestRemovePath(t *testing.T) {
+	tests := []struct {
+		name           string
+		prefix         net.Prefix
+		path           *route.Path
+		term           *Term
+		exptectCalled  bool
+		expectModified bool
+	}{
+		{
+			name:   "accept",
+			prefix: net.NewPfx(0, 0),
+			path:   &route.Path{},
+			term: &Term{
+				then: []Then{
+					&actions.AcceptAction{},
+				},
+			},
+			exptectCalled:  true,
+			expectModified: false,
+		},
+		{
+			name:   "reject",
+			prefix: net.NewPfx(0, 0),
+			path:   &route.Path{},
+			term: &Term{
+				then: []Then{
+					&actions.RejectAction{},
+				},
+			},
+			exptectCalled:  false,
+			expectModified: false,
+		},
+		{
+			name:   "modified",
+			prefix: net.NewPfx(0, 0),
+			path:   &route.Path{},
+			term: &Term{
+				then: []Then{
+					&mockAction{},
+					&actions.AcceptAction{},
+				},
+			},
+			exptectCalled:  true,
+			expectModified: true,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(te *testing.T) {
+			m := newClientMock()
+
+			f := NewFilter([]*Term{test.term})
+			f.Register(m)
+
+			f.RemovePath(test.prefix, test.path)
+			assert.Equal(te, test.exptectCalled, m.removePathCalled, "called")
+
+			if !test.exptectCalled {
+				return
+			}
+
+			if m.path != test.path && !test.expectModified {
+				te.Fatal("expected path to be not modified but was")
+			}
+
+			if m.path == test.path && test.expectModified {
+				te.Fatal("expected path to be modified but was same reference")
+			}
+		})
+	}
+}
diff --git a/routingtable/filter/from.go b/routingtable/filter/from.go
new file mode 100644
index 0000000000000000000000000000000000000000..2237fe4ea9fcc61e65f2ec8c59a400e619fc092f
--- /dev/null
+++ b/routingtable/filter/from.go
@@ -0,0 +1,14 @@
+package filter
+
+import (
+	"github.com/bio-routing/bio-rd/net"
+	"github.com/bio-routing/bio-rd/route"
+)
+
+type From struct {
+	prefixList *PrefixList
+}
+
+func (f *From) Matches(p net.Prefix, pa *route.Path) bool {
+	return f.prefixList.Matches(p)
+}
diff --git a/routingtable/filter/prefix_list.go b/routingtable/filter/prefix_list.go
new file mode 100644
index 0000000000000000000000000000000000000000..b783a07a540b67acc7fd3384b582490f0bb5aea2
--- /dev/null
+++ b/routingtable/filter/prefix_list.go
@@ -0,0 +1,17 @@
+package filter
+
+import "github.com/bio-routing/bio-rd/net"
+
+type PrefixList struct {
+	allowed []net.Prefix
+}
+
+func (f *PrefixList) Matches(p net.Prefix) bool {
+	for _, a := range f.allowed {
+		if !a.Contains(p) {
+			return false
+		}
+	}
+
+	return true
+}
diff --git a/routingtable/filter/term.go b/routingtable/filter/term.go
new file mode 100644
index 0000000000000000000000000000000000000000..85a88eb88945581c388f790bcf17631b7f1da9db
--- /dev/null
+++ b/routingtable/filter/term.go
@@ -0,0 +1,52 @@
+package filter
+
+import (
+	"github.com/bio-routing/bio-rd/net"
+	"github.com/bio-routing/bio-rd/route"
+)
+
+// Term matches a path against a list of conditions and performs actions if it matches
+type Term struct {
+	from []*From
+	then []Then
+}
+
+// NewTerm creates a new term
+func NewTerm(from []*From, then []Then) *Term {
+	t := &Term{
+		from: from,
+		then: then,
+	}
+
+	return t
+}
+
+// Process processes a path returning if the path should be rejected and returns a possible modified version of the path
+func (t *Term) Process(p net.Prefix, pa *route.Path) (modPath *route.Path, reject bool) {
+	orig := pa
+
+	if len(t.from) == 0 {
+		return t.processActions(p, pa)
+	}
+
+	for _, f := range t.from {
+		if f.Matches(p, pa) {
+			return t.processActions(p, pa)
+		}
+	}
+
+	return orig, true
+}
+
+func (t *Term) processActions(p net.Prefix, pa *route.Path) (modPath *route.Path, reject bool) {
+	modPath = pa
+
+	for _, action := range t.then {
+		modPath, reject = action.Do(p, modPath)
+		if reject {
+			continue
+		}
+	}
+
+	return modPath, reject
+}
diff --git a/routingtable/filter/term_test.go b/routingtable/filter/term_test.go
new file mode 100644
index 0000000000000000000000000000000000000000..ee55c712c21a055d75dc61468751e1c80b92f81b
--- /dev/null
+++ b/routingtable/filter/term_test.go
@@ -0,0 +1,169 @@
+package filter
+
+import (
+	"testing"
+
+	"github.com/bio-routing/bio-rd/net"
+	"github.com/bio-routing/bio-rd/route"
+	"github.com/bio-routing/bio-rd/routingtable/filter/actions"
+	"github.com/stretchr/testify/assert"
+)
+
+type mockAction struct {
+}
+
+func (*mockAction) Do(p net.Prefix, pa *route.Path) (*route.Path, bool) {
+	cp := *pa
+	cp.Type = route.OSPFPathType
+
+	return &cp, false
+}
+
+func TestProcess(t *testing.T) {
+	tests := []struct {
+		name           string
+		prefix         net.Prefix
+		path           *route.Path
+		from           []*From
+		then           []Then
+		expectReject   bool
+		expectModified bool
+	}{
+		{
+			name:   "empty from",
+			prefix: net.NewPfx(strAddr("100.64.0.1"), 8),
+			path:   &route.Path{},
+			from:   []*From{},
+			then: []Then{
+				&actions.AcceptAction{},
+			},
+			expectReject:   false,
+			expectModified: false,
+		},
+		{
+			name:   "from matches",
+			prefix: net.NewPfx(strAddr("100.64.0.1"), 8),
+			path:   &route.Path{},
+			from: []*From{
+				{
+					&PrefixList{
+						allowed: []net.Prefix{
+							net.NewPfx(0, 0),
+						},
+					},
+				},
+			},
+			then: []Then{
+				&actions.AcceptAction{},
+			},
+			expectReject:   false,
+			expectModified: false,
+		},
+		{
+			name:   "from does not match",
+			prefix: net.NewPfx(strAddr("100.64.0.1"), 8),
+			path:   &route.Path{},
+			from: []*From{
+				{
+					&PrefixList{
+						allowed: []net.Prefix{
+							net.NewPfx(0, 32),
+						},
+					},
+				},
+			},
+			then: []Then{
+				&actions.AcceptAction{},
+			},
+			expectReject:   true,
+			expectModified: false,
+		},
+		{
+			name:   "modified",
+			prefix: net.NewPfx(strAddr("100.64.0.1"), 8),
+			path:   &route.Path{},
+			from: []*From{
+				{
+					&PrefixList{
+						allowed: []net.Prefix{
+							net.NewPfx(0, 0),
+						},
+					},
+				},
+			},
+			then: []Then{
+				&mockAction{},
+			},
+			expectReject:   false,
+			expectModified: true,
+		},
+		{
+			name:   "modified and accepted (2 actions)",
+			prefix: net.NewPfx(strAddr("100.64.0.1"), 8),
+			path:   &route.Path{},
+			from: []*From{
+				{
+					&PrefixList{
+						allowed: []net.Prefix{
+							net.NewPfx(0, 0),
+						},
+					},
+				},
+			},
+			then: []Then{
+				&mockAction{},
+				&actions.AcceptAction{},
+			},
+			expectReject:   false,
+			expectModified: true,
+		},
+		{
+			name:   "one of the prefix filters matches",
+			prefix: net.NewPfx(strAddr("100.64.0.1"), 8),
+			path:   &route.Path{},
+			from: []*From{
+				{
+					&PrefixList{
+						allowed: []net.Prefix{
+							net.NewPfx(0, 32),
+						},
+					},
+				},
+				{
+					&PrefixList{
+						allowed: []net.Prefix{
+							net.NewPfx(0, 0),
+						},
+					},
+				},
+			},
+			then: []Then{
+				&actions.AcceptAction{},
+			},
+			expectReject:   false,
+			expectModified: false,
+		},
+	}
+
+	for _, test := range tests {
+		t.Run(test.name, func(te *testing.T) {
+			term := NewTerm(test.from, test.then)
+
+			pa, reject := term.Process(test.prefix, test.path)
+			assert.Equal(te, test.expectReject, reject, "reject")
+
+			if pa != test.path && !test.expectModified {
+				te.Fatal("expected path to be not modified but was")
+			}
+
+			if pa == test.path && test.expectModified {
+				te.Fatal("expected path to be modified but was same reference")
+			}
+		})
+	}
+}
+
+func strAddr(s string) uint32 {
+	ret, _ := net.StrToAddr(s)
+	return ret
+}
diff --git a/routingtable/filter/then.go b/routingtable/filter/then.go
new file mode 100644
index 0000000000000000000000000000000000000000..920daf43c1cdb121400168cc0ac17460bff42350
--- /dev/null
+++ b/routingtable/filter/then.go
@@ -0,0 +1,10 @@
+package filter
+
+import (
+	"github.com/bio-routing/bio-rd/net"
+	"github.com/bio-routing/bio-rd/route"
+)
+
+type Then interface {
+	Do(p net.Prefix, pa *route.Path) (modPath *route.Path, reject bool)
+}