Files
higress/plugins/wasm-go/extensions/geo-ip/main.go

266 lines
6.7 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package main
import (
"errors"
"net"
"net/url"
"strings"
_ "embed"
"github.com/alibaba/higress/plugins/wasm-go/pkg/wrapper"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm"
"github.com/higress-group/proxy-wasm-go-sdk/proxywasm/types"
"github.com/tidwall/gjson"
"github.com/zmap/go-iptree/iptree"
)
//go:embed geoCidr.txt
var geoipdata string
var GeoIpRdxTree *iptree.IPTree
var HaveInitGeoIpDb bool = false
const (
DefaultRealIpHeader = "X-Forwarded-For"
OriginSourceType = "origin-source"
HeaderSourceType = "header"
)
// 根据ip2region 项目里的ip地理位置数据库ip.merge.txt的内网ip网段经过ip网段转换多个cidr的程序 geo-ip/generateCidr/ipRange2Cidr.go 转换成的cidr地址。
var internalIpCidr []string = []string{"0.0.0.0/8", "10.0.0.0/8", "100.64.0.0/11", "100.96.0.0/12",
"100.112.0.0/13", "100.120.0.0/15", "100.122.0.0/16", "100.123.0.0/16", "100.124.0.0/14",
"127.0.0.0/8", "169.254.0.0/16", "172.16.0.0/12", "192.0.0.0/24", "192.0.2.0/24", "192.88.99.0/24",
"192.168.0.0/16", "198.18.0.0/15", "198.51.100.0/24", "203.0.113.0/24", "224.0.0.0/3",
}
func main() {
wrapper.SetCtx(
"geo-ip",
wrapper.ParseConfigBy(parseConfig),
wrapper.ProcessRequestHeadersBy(onHttpRequestHeaders),
)
}
type GeoIpConfig struct {
IpProtocol string `json:"ip_protocol"`
IPSourceType string `json:"ip_source_type"`
IPHeaderName string `json:"ip_header_name"`
}
type GeoIpData struct {
Cidr string `json:"cidr"`
Country string `json:"country"`
Province string `json:"province"`
City string `json:"city"`
Isp string `json:"isp"`
}
func parseConfig(json gjson.Result, config *GeoIpConfig, log wrapper.Log) error {
sourceType := json.Get("ip_source_type")
if sourceType.Exists() && sourceType.String() != "" {
switch sourceType.String() {
case HeaderSourceType:
config.IPSourceType = HeaderSourceType
case OriginSourceType:
default:
config.IPSourceType = OriginSourceType
}
} else {
config.IPSourceType = OriginSourceType
}
header := json.Get("ip_header_name")
if header.Exists() && header.String() != "" {
config.IPHeaderName = header.String()
} else {
config.IPHeaderName = DefaultRealIpHeader
}
ipProtocol := json.Get("ip_protocol")
if !ipProtocol.Exists() {
config.IpProtocol = "ipv4"
} else {
switch ipProtocol.String() {
case "ipv6":
config.IpProtocol = "ipv6"
case "ipv4":
default:
config.IpProtocol = "ipv4"
}
}
if HaveInitGeoIpDb {
return nil
}
if err := ReadGeoIpDataToRdxtree(log); err != nil {
log.Errorf("read geoip data failed.%v", err)
return err
}
HaveInitGeoIpDb = true
return nil
}
func ReadGeoIpDataToRdxtree(log wrapper.Log) error {
GeoIpRdxTree = iptree.New()
//eg., cidr country province city isp
geoIpRows := strings.Split(geoipdata, "\n")
for _, row := range geoIpRows {
if row == "" {
log.Infof("parsed empty line.")
continue
}
pureRow := strings.Trim(row, " ")
tmpArr := strings.Split(pureRow, "|")
if len(tmpArr) < 5 {
return errors.New("geoIp row field number is less than 5 " + row)
}
cidr := strings.Trim(tmpArr[0], " ")
geoIpData := &GeoIpData{
Cidr: cidr,
Country: strings.Trim(tmpArr[1], " "),
Province: strings.Trim(tmpArr[2], " "),
City: strings.Trim(tmpArr[3], " "),
Isp: strings.Trim(tmpArr[4], " "),
}
if err := GeoIpRdxTree.AddByString(cidr, geoIpData); err != nil {
return errors.New("add geoipdata into radix treefailed " + err.Error())
}
log.Debugf("added geoip data into radixtree: %v", *geoIpData)
}
return nil
}
// search geodata using client ip in radixtree.
func SearchGeoIpDataInRdxtree(ip string, log wrapper.Log) (*GeoIpData, error) {
val, found, err := GeoIpRdxTree.GetByString(ip)
if err != nil {
log.Errorf("search geo ip data in raditree failed. %v %s", err, ip)
return nil, err
}
if found {
return val.(*GeoIpData), nil
}
return nil, errors.New("geo ip data not found")
}
func parseIP(source string) string {
if strings.Contains(source, ".") {
// parse ipv4
return strings.Split(source, ":")[0]
}
//parse ipv6
if strings.Contains(source, "]") {
return strings.Split(source, "]")[0][1:]
}
return source
}
func isInternalIp(ip string) (string, error) {
if ip == "" {
return "", errors.New("empty ip")
}
ipBt := net.ParseIP(ip)
if ipBt == nil {
return "", errors.New("invalid ip format")
}
ip4B := ipBt.To4()
if ip4B == nil {
return "", errors.New("not ipv4 format")
}
for _, cidr := range internalIpCidr {
_, networkIp, err := net.ParseCIDR(cidr)
if err != nil {
return "", err
}
if networkIp.Contains(ip4B) {
return cidr, nil
}
}
return "", nil
}
func onHttpRequestHeaders(ctx wrapper.HttpContext, config GeoIpConfig, log wrapper.Log) types.Action {
var (
s string
err error
)
if config.IPSourceType == HeaderSourceType {
s, err = proxywasm.GetHttpRequestHeader(config.IPHeaderName)
if err == nil {
s = strings.Split(strings.Trim(s, " "), ",")[0]
}
} else {
var bs []byte
bs, err = proxywasm.GetProperty([]string{"source", "address"})
s = string(bs)
}
if err != nil {
log.Errorf("get client ip failed. %s %v", config.IPSourceType, err)
return types.ActionContinue
}
clientIp := parseIP(s)
//ipv6 will be implemented in the future.
if config.IpProtocol == "ipv6" || strings.Contains(clientIp, ":") {
log.Warnf("ipv6 will be implemented in the future.%s %s", clientIp, config.IpProtocol)
return types.ActionContinue
}
internalCidr, err := isInternalIp(clientIp)
if err != nil {
log.Errorf("check internal ip failed. error: %v", err)
return types.ActionContinue
}
var geoIpData *GeoIpData
if internalCidr != "" {
geoIpData = &GeoIpData{
Cidr: internalCidr,
City: "内网IP",
Province: "内网IP",
Country: "内网IP",
Isp: "内网IP",
}
} else {
geoIpData, err = SearchGeoIpDataInRdxtree(clientIp, log)
if err != nil {
log.Errorf("search geo info failed.%v", err)
return types.ActionContinue
}
}
proxywasm.SetProperty([]string{"geo-city"}, []byte(geoIpData.City))
proxywasm.SetProperty([]string{"geo-province"}, []byte(geoIpData.Province))
proxywasm.SetProperty([]string{"geo-country"}, []byte(geoIpData.Country))
proxywasm.SetProperty([]string{"geo-isp"}, []byte(geoIpData.Isp))
countryEnc := url.QueryEscape(geoIpData.Country)
provinceEnc := url.QueryEscape(geoIpData.Province)
cityEnc := url.QueryEscape(geoIpData.City)
ispEnc := url.QueryEscape(geoIpData.Isp)
proxywasm.AddHttpRequestHeader("X-Higress-Geo-Country", countryEnc)
proxywasm.AddHttpRequestHeader("X-Higress-Geo-Province", provinceEnc)
proxywasm.AddHttpRequestHeader("X-Higress-Geo-City", cityEnc)
proxywasm.AddHttpRequestHeader("X-Higress-Geo-Isp", ispEnc)
return types.ActionContinue
}