aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMike Crute <mike@crute.us>2022-05-21 19:31:43 -0700
committerMike Crute <mike@crute.us>2022-05-21 19:33:34 -0700
commit32497734221f529846415ad34c1979fed8e98c5c (patch)
treef9e985663ca73ea3097007166340d832f5e40426
parent43ea770a0f42d650864ec313a26cf0f1a3824f1c (diff)
downloadgolib-32497734221f529846415ad34c1979fed8e98c5c.tar.bz2
golib-32497734221f529846415ad34c1979fed8e98c5c.tar.xz
golib-32497734221f529846415ad34c1979fed8e98c5c.zip
echo: add netbox-based IP filter
-rw-r--r--echo/middleware/ip_filter.go141
-rw-r--r--echo/netbox/client.go169
-rw-r--r--echo/netbox/model.go74
3 files changed, 361 insertions, 23 deletions
diff --git a/echo/middleware/ip_filter.go b/echo/middleware/ip_filter.go
index 007791e..2d79925 100644
--- a/echo/middleware/ip_filter.go
+++ b/echo/middleware/ip_filter.go
@@ -1,39 +1,134 @@
1package middleware 1package middleware
2 2
3import ( 3import (
4 "context"
5 "fmt"
4 "net" 6 "net"
7 "sync"
8 "time"
5 9
10 "code.crute.us/mcrute/golib/echo/netbox"
6 "github.com/labstack/echo/v4" 11 "github.com/labstack/echo/v4"
12 "github.com/prometheus/client_golang/prometheus"
13 "github.com/prometheus/client_golang/prometheus/promauto"
7) 14)
8 15
9func NewIPFilter(allowedRanges []*net.IPNet) echo.MiddlewareFunc { 16var netboxFilterFailures = promauto.NewCounter(prometheus.CounterOpts{
10 return func(next echo.HandlerFunc) echo.HandlerFunc { 17 Name: "netbox_ip_filter_refresh_failures",
11 return func(c echo.Context) error { 18 Help: "Total number of failures refreshing netbox sourced IP ranges",
12 if allowedRanges == nil { 19})
13 c.Logger().Error("No allowed IPs configured for filter")
14 return echo.ErrNotFound
15 }
16 20
17 ip := net.ParseIP(c.RealIP()) 21type ipFilter struct {
18 if ip == nil { 22 sync.RWMutex
19 c.Logger().Error("Unable to parse IP in IPFilter") 23 allowLocalhost bool
20 return echo.ErrNotFound 24 allowedRanges []*net.IPNet
21 } 25}
22 26
23 found := false 27func (f *ipFilter) UpdateRanges(r []*net.IPNet) {
24 for _, ipnet := range allowedRanges { 28 f.Lock()
25 if ipnet.Contains(ip) { 29 defer f.Unlock()
26 found = true 30 f.allowedRanges = r
27 break 31}
28 } 32
29 } 33func (f *ipFilter) Middleware(next echo.HandlerFunc) echo.HandlerFunc {
34 _, v4Localhost, _ := net.ParseCIDR("127.0.0.0/8")
35 _, v6Localhost, _ := net.ParseCIDR("::1/128")
36
37 return func(c echo.Context) error {
38 f.RLock()
39 defer f.RUnlock()
40
41 if f.allowedRanges == nil {
42 c.Logger().Error("No allowed IPs configured for filter")
43 return echo.ErrNotFound
44 }
30 45
31 if !found { 46 ip := net.ParseIP(c.RealIP())
32 c.Logger().Errorf("IP %s not in range for filter", c.RealIP()) 47 if ip == nil {
33 return echo.ErrNotFound 48 c.Logger().Error("Unable to parse IP in IPFilter")
49 return echo.ErrNotFound
50 }
51
52 found := false
53 for _, ipnet := range f.allowedRanges {
54 if ipnet.Contains(ip) {
55 found = true
56 break
34 } 57 }
58 }
59
60 if f.allowLocalhost && (v4Localhost.Contains(ip) || v6Localhost.Contains(ip)) {
61 found = true
62 }
63
64 if !found {
65 c.Logger().Errorf("IP %s not in range for filter", c.RealIP())
66 return echo.ErrNotFound
67 }
68
69 return next(c)
70 }
71}
35 72
36 return next(c) 73func NewIPFilter(allowedRanges []*net.IPNet) echo.MiddlewareFunc {
74 return (&ipFilter{allowedRanges: allowedRanges}).Middleware
75}
76
77type NetboxIPFilter struct {
78 NetboxClient netbox.NetboxClient
79 Tag string
80 IncludeLocalhost bool
81 Logger echo.Logger
82 f *ipFilter
83 hasInit bool
84}
85
86func (f *NetboxIPFilter) Init() error {
87 nets, err := f.NetboxClient.GetPrefixesWithTag(f.Tag)
88 if err != nil {
89 return err
90 }
91 f.Logger.Debugf("Got prefixes: %s", nets)
92
93 f.f = &ipFilter{
94 allowedRanges: nets,
95 allowLocalhost: f.IncludeLocalhost,
96 }
97 f.hasInit = true
98
99 return nil
100}
101
102func (f *NetboxIPFilter) Middleware(next echo.HandlerFunc) echo.HandlerFunc {
103 return f.f.Middleware(next)
104}
105
106func (f *NetboxIPFilter) RunRefresh(c context.Context, wg *sync.WaitGroup) error {
107 wg.Add(1)
108 defer wg.Done()
109
110 if !f.hasInit {
111 return fmt.Errorf("NetboxIPFilter: has not been initialized before RunRefresh called")
112 }
113
114 f.Logger.Info("Starting netbox IP address filter refresh loop")
115
116 t := time.NewTicker(time.Hour)
117 defer t.Stop()
118
119 for {
120 select {
121 case <-t.C:
122 if nets, err := f.NetboxClient.GetPrefixesWithTag(f.Tag); err != nil {
123 f.Logger.Errorf("Error refreshing netbox prefixes for IP filter: %w", err)
124 netboxFilterFailures.Inc()
125 } else {
126 f.Logger.Debugf("Got prefixes: %s", nets)
127 f.f.UpdateRanges(nets)
128 }
129 case <-c.Done():
130 f.Logger.Info("Shutting down netbox IP address filter refresh loop")
131 return nil
37 } 132 }
38 } 133 }
39} 134}
diff --git a/echo/netbox/client.go b/echo/netbox/client.go
new file mode 100644
index 0000000..aabadc8
--- /dev/null
+++ b/echo/netbox/client.go
@@ -0,0 +1,169 @@
1package netbox
2
3import (
4 "encoding/json"
5 "fmt"
6 "io"
7 "net"
8 "net/http"
9 "net/url"
10 "strconv"
11
12 "code.crute.us/mcrute/golib/vault"
13)
14
15type NetboxClient interface {
16 GetSitePrefixesWithTag(site string, tag string) ([]*net.IPNet, error)
17 GetPrefixesWithTag(tag string) ([]*net.IPNet, error)
18}
19
20func NewNetboxClientDefault() NetboxClient {
21 return &netboxClient{
22 endpoint: "https://netbox.crute.me",
23 vaultMaterial: "infra/netbox-readonly",
24 }
25}
26
27func NewNetboxClient(endpoint string, vaultMaterial string) NetboxClient {
28 return &netboxClient{
29 endpoint: endpoint,
30 vaultMaterial: vaultMaterial,
31 }
32}
33
34type netboxClient struct {
35 endpoint string
36 vaultMaterial string
37}
38
39func (c *netboxClient) makeRequestRaw(method, u string, ib io.Reader, o interface{}) error {
40 apiKey, err := vault.GetVaultApiKey(c.vaultMaterial)
41 if err != nil {
42 return err
43 }
44
45 req, err := http.NewRequest(method, u, ib)
46 if err != nil {
47 return err
48 }
49 req.Header.Add("Authorization", fmt.Sprintf("Token %s", apiKey))
50
51 res, err := http.DefaultClient.Do(req)
52 if err != nil {
53 return err
54 }
55 defer res.Body.Close()
56
57 if err = json.NewDecoder(res.Body).Decode(o); err != nil {
58 return err
59 }
60
61 return nil
62}
63
64func (c *netboxClient) makeRequest(method, path string, q url.Values, ib io.Reader, o interface{}) error {
65 u, err := url.Parse(c.endpoint)
66 if err != nil {
67 return err
68 }
69 u.Path = path
70 u.RawQuery = q.Encode()
71
72 return c.makeRequestRaw(method, u.String(), ib, o)
73}
74
75func (c *netboxClient) GetSitePrefixesWithTag(site string, tag string) ([]*net.IPNet, error) {
76 s, err := c.resolveSiteNameToId(site)
77 if err != nil {
78 return nil, err
79 }
80
81 out := []*net.IPNet{}
82
83 q := url.Values{}
84 q.Add("site_id", strconv.Itoa(s))
85 q.Add("tag", tag)
86
87 page := &PrefixList{}
88 if err := c.makeRequest(http.MethodGet, "/api/ipam/prefixes/", q, nil, page); err != nil {
89 return nil, err
90 }
91
92 for _, r := range page.Results {
93 _, ipnet, err := net.ParseCIDR(r.Prefix)
94 if err != nil {
95 return nil, err
96 }
97 out = append(out, ipnet)
98 }
99
100 for page.Next != "" {
101 page = &PrefixList{}
102 if err := c.makeRequestRaw(http.MethodGet, page.Next, nil, page); err != nil {
103 return nil, err
104 }
105
106 for _, r := range page.Results {
107 _, ipnet, err := net.ParseCIDR(r.Prefix)
108 if err != nil {
109 return nil, err
110 }
111 out = append(out, ipnet)
112 }
113 }
114
115 return out, nil
116}
117
118func (c *netboxClient) GetPrefixesWithTag(tag string) ([]*net.IPNet, error) {
119 out := []*net.IPNet{}
120
121 q := url.Values{}
122 q.Add("tag", tag)
123
124 page := &PrefixList{}
125 if err := c.makeRequest(http.MethodGet, "/api/ipam/prefixes/", q, nil, page); err != nil {
126 return nil, err
127 }
128
129 for _, r := range page.Results {
130 _, ipnet, err := net.ParseCIDR(r.Prefix)
131 if err != nil {
132 return nil, err
133 }
134 out = append(out, ipnet)
135 }
136
137 for page.Next != "" {
138 page = &PrefixList{}
139 if err := c.makeRequestRaw(http.MethodGet, page.Next, nil, page); err != nil {
140 return nil, err
141 }
142
143 for _, r := range page.Results {
144 _, ipnet, err := net.ParseCIDR(r.Prefix)
145 if err != nil {
146 return nil, err
147 }
148 out = append(out, ipnet)
149 }
150 }
151
152 return out, nil
153}
154
155func (c *netboxClient) resolveSiteNameToId(s string) (int, error) {
156 q := url.Values{}
157 q.Add("name", s)
158
159 out := &SiteList{}
160 if err := c.makeRequest(http.MethodGet, "/api/dcim/sites/", q, nil, out); err != nil {
161 return 0, err
162 }
163
164 if len(out.Results) == 0 {
165 return 0, fmt.Errorf("resolveSiteNameToId: no results returned from netbox")
166 }
167
168 return out.Results[0].ID, nil
169}
diff --git a/echo/netbox/model.go b/echo/netbox/model.go
new file mode 100644
index 0000000..78f2ca6
--- /dev/null
+++ b/echo/netbox/model.go
@@ -0,0 +1,74 @@
1package netbox
2
3type LabeledInt struct {
4 Value int `json:"value"`
5 Label string `json:"label"`
6}
7
8type LabeledString struct {
9 Value string `json:"value"`
10 Label string `json:"label"`
11}
12
13type Role struct {
14 ID int `json:"ID"`
15 Url string `json:"url"`
16 Display string `json:"display"`
17 Name string `json:"name"`
18 Slug string `json:"slub"`
19}
20
21type Tag struct {
22 ID int `json:"id"`
23 Url string `json:"url"`
24 Display string `json:"display"`
25 Name string `json:"name"`
26 Slug string `json:"slug"`
27 Color string `json:"color"`
28}
29
30type SiteList struct {
31 Count int `json:"count"`
32 Next string `json:"next"`
33 Previous string `json:"previous"`
34 Results []*Site `json:"results"`
35}
36
37type Site struct {
38 ID int `json:"id"`
39 Url string `json:"url"`
40 Display string `json:"display"`
41 Name string `json:"name"`
42 Slug string `json:"slug"`
43 Facility string `json:"facility"`
44 Description string `json:"description"`
45 Timezone string `json:"time_zone"`
46 ASN int `json:"asn"`
47 Status LabeledString `json:"status"`
48}
49
50type PrefixList struct {
51 Count int `json:"count"`
52 Next string `json:"next"`
53 Previous string `json:"previous"`
54 Results []*Prefix `json:"results"`
55}
56
57type Prefix struct {
58 ID int `json:"ID"`
59 Url string `json:"url"`
60 Display string `json:"display"`
61 Prefix string `json:"prefix"`
62 IsPool bool `json:"is_pool"`
63 Description string `json:"description"`
64 Created string `json:"created"`
65 LastUpdated string `json:"last_updated"`
66 Children int `json:"children"`
67 Depth int `json:"_depth"`
68 Family LabeledInt `json:"family"`
69 Status LabeledString `json:"status"`
70 Site *Site `json:"site"`
71 Role *Role `json:"role"`
72 Tags []*Tag `json:"tags"`
73 CustomFields map[string]interface{} `json:"custom_fields"`
74}