Skip to content

Commit

Permalink
feat: support IP allow list (#1424)
Browse files Browse the repository at this point in the history
  • Loading branch information
nic-chen authored Feb 5, 2021
1 parent 4577740 commit 42f3bf6
Show file tree
Hide file tree
Showing 8 changed files with 200 additions and 4 deletions.
1 change: 1 addition & 0 deletions .github/workflows/backend-e2e-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ jobs:
sed -i 's/127.0.0.1/0.0.0.0/' ./api/conf/conf.yaml
sed -i '/172.16.238.10:2379/a\ - 172.16.238.11:2379' ./api/conf/conf.yaml
sed -i '/172.16.238.10:2379/a\ - 172.16.238.12:2379' ./api/conf/conf.yaml
sed -i 's@127.0.0.0/24@0.0.0.0/0@' ./api/conf/conf.yaml
- name: download file Dockerfile-apisix
working-directory: ./api/test/docker
Expand Down
2 changes: 2 additions & 0 deletions api/conf/conf.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@ conf:
listen:
host: 127.0.0.1 # `manager api` listening ip or host name
port: 9000 # `manager api` listening port
allow_list: # If we don't set any IP list, then any IP access is allowed by default.
- 127.0.0.0/24
etcd:
endpoints: # supports defining multiple etcd host addresses for an etcd cluster
- 127.0.0.1:2379
Expand Down
10 changes: 7 additions & 3 deletions api/internal/conf/conf.go
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ var (
SSLDefaultStatus = 1 //enable ssl by default
ImportSizeLimit = 10 * 1024 * 1024
PIDPath = "/tmp/manager-api.pid"
AllowList []string
)

type Etcd struct {
Expand Down Expand Up @@ -82,9 +83,10 @@ type Log struct {
}

type Conf struct {
Etcd Etcd
Listen Listen
Log Log
Etcd Etcd
Listen Listen
Log Log
AllowList []string `yaml:"allow_list"`
}

type User struct {
Expand Down Expand Up @@ -173,6 +175,8 @@ func setConf() {
}
}

AllowList = config.Conf.AllowList

//auth
initAuthentication(config.Authentication)
}
Expand Down
105 changes: 105 additions & 0 deletions api/internal/filter/ip_filter.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package filter

import (
"net"
"net/http"

"github.com/gin-gonic/gin"

"github.com/apisix/manager-api/internal/conf"
"github.com/apisix/manager-api/internal/log"
"github.com/apisix/manager-api/internal/utils/consts"
)

type subnet struct {
ipStr string
ipNet *net.IPNet
allowed bool
}

func generateIPSet(ipList []string) (map[string]bool, []*subnet) {
var ips = map[string]bool{}
var subnets []*subnet
for _, ipStr := range ipList {
if ip, net, err := net.ParseCIDR(ipStr); err == nil {
if n, total := net.Mask.Size(); n == total {
ips[ip.String()] = true
continue
}

subnets = append(subnets, &subnet{
ipStr: ipStr,
ipNet: net,
allowed: true,
})
continue
}
if ip := net.ParseIP(ipStr); ip != nil {
ips[ip.String()] = true
}
}

return ips, subnets
}

func checkIP(ipStr string, ips map[string]bool, subnets []*subnet) bool {
allowed, ok := ips[ipStr]
if ok {
return allowed
}

ip := net.ParseIP(ipStr)
if ip == nil {
return false
}

for _, subnet := range subnets {
if subnet.ipNet.Contains(ip) {
return subnet.allowed
}
}

return false
}

func IPFilter() gin.HandlerFunc {
ips, subnets := generateIPSet(conf.AllowList)
return func(c *gin.Context) {
ipStr := c.ClientIP()

if len(conf.AllowList) < 1 {
c.Next()
return
}

if ipStr == "" {
log.Warn("forbidden by empty IP")
c.AbortWithStatusJSON(http.StatusForbidden, consts.ErrIPNotAllow)
return
}

res := checkIP(ipStr, ips, subnets)
if !res {
log.Warnf("forbidden by IP: %s, allowed list: %v", ipStr, conf.AllowList)
c.AbortWithStatusJSON(http.StatusForbidden, consts.ErrIPNotAllow)
}

c.Next()
}
}
58 changes: 58 additions & 0 deletions api/internal/filter/ip_filter_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package filter

import (
"testing"

"github.com/gin-gonic/gin"
"github.com/stretchr/testify/assert"

"github.com/apisix/manager-api/internal/conf"
)

func TestIPFilter_Handle(t *testing.T) {
// empty allowed ip list --> should normal
conf.AllowList = []string{}
r := gin.New()
r.Use(IPFilter())

r.GET("/", func(c *gin.Context) {
})

w := performRequest(r, "GET", "/")
assert.Equal(t, 200, w.Code)

// should forbidden
conf.AllowList = []string{"10.0.0.0/8", "10.0.0.1"}
r = gin.New()
r.Use(IPFilter())
r.GET("/fbd", func(c *gin.Context) {
})

w = performRequest(r, "GET", "/fbd")
assert.Equal(t, 403, w.Code)

// should allowed
conf.AllowList = []string{"10.0.0.0/8", "0.0.0.0/0"}
r = gin.New()
r.Use(IPFilter())
r.GET("/test", func(c *gin.Context) {
})
w = performRequest(r, "GET", "/test")
assert.Equal(t, 200, w.Code)
}
2 changes: 1 addition & 1 deletion api/internal/route.go
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ func SetUpRouter() *gin.Engine {
logger := log.GetLogger(log.AccessLog)
store := cookie.NewStore([]byte("secret"))
r.Use(sessions.Sessions("session", store))
r.Use(filter.CORS(), filter.RequestId(), filter.RequestLogHandler(logger), filter.SchemaCheck(), filter.RecoverHandler())
r.Use(filter.CORS(), filter.RequestId(), filter.IPFilter(), filter.RequestLogHandler(logger), filter.SchemaCheck(), filter.RecoverHandler())
r.Use(static.Serve("/", static.LocalFile(filepath.Join(conf.WorkDir, conf.WebDir), false)))
r.NoRoute(func(c *gin.Context) {
c.File(fmt.Sprintf("%s/index.html", filepath.Join(conf.WorkDir, conf.WebDir)))
Expand Down
2 changes: 2 additions & 0 deletions api/internal/utils/consts/error.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,10 +20,12 @@ import "github.com/shiningrush/droplet/data"

const (
ErrBadRequest = 20001
ErrForbidden = 20002
)

var (
// base error please refer to github.com/shiningrush/droplet/data, such as data.ErrNotFound, data.ErrConflicted
ErrInvalidRequest = data.BaseError{Code: ErrBadRequest, Message: "invalid request"}
ErrSchemaValidateFailed = data.BaseError{Code: ErrBadRequest, Message: "JSONSchema validate failed"}
ErrIPNotAllow = data.BaseError{Code: ErrForbidden, Message: "IP address not allowed"}
)
24 changes: 24 additions & 0 deletions api/test/shell/cli_test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -246,6 +246,30 @@ if [[ `grep -c "/apisix/admin/user/login" ./logs/access.log` -eq '0' ]]; then
exit 1
fi

# clean config
clean_up

# set ip allowed list
if [[ $KERNEL = "Darwin" ]]; then
sed -i "" 's@127.0.0.0/24@10.0.0.1@' conf/conf.yaml
else
sed -i 's@127.0.0.0/24@10.0.0.1@' conf/conf.yaml
fi

./manager-api &
sleep 3

# should be forbidden
curl http://127.0.0.1:9000
code=$(curl -k -i -m 20 -o /dev/null -s -w %{http_code} http://127.0.0.1:9000)
if [ ! $code -eq 403 ]; then
echo "failed: verify IP allowed list failed"
exit 1
fi

./manager-api stop
clean_up


# etcd basic auth
# add root user
Expand Down

0 comments on commit 42f3bf6

Please sign in to comment.