2020-08-25 07:29:10 +00:00
|
|
|
package iplist
|
|
|
|
|
|
|
|
import (
|
|
|
|
"errors"
|
|
|
|
"fmt"
|
|
|
|
"net"
|
|
|
|
"os"
|
|
|
|
"strings"
|
|
|
|
"time"
|
|
|
|
)
|
|
|
|
|
|
|
|
var fields []string
|
|
|
|
var initialized bool
|
|
|
|
|
|
|
|
// Init should be called with domain that has valid SPF-like records
|
|
|
|
// to populate the IP whitelist, or with an empty string "" to disable
|
|
|
|
func Init(txtDomain string) []string {
|
|
|
|
initialized = true
|
|
|
|
if "" == txtDomain {
|
|
|
|
return []string{}
|
|
|
|
}
|
|
|
|
|
|
|
|
err := updateTxt(txtDomain)
|
|
|
|
if nil != err {
|
|
|
|
panic(err)
|
|
|
|
}
|
|
|
|
go func() {
|
|
|
|
for {
|
|
|
|
time.Sleep(5 * time.Minute)
|
|
|
|
if err := updateTxt(txtDomain); nil != err {
|
|
|
|
fmt.Fprintf(os.Stderr, "warn: could not update iplist: %s\n", err)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}()
|
|
|
|
|
|
|
|
for _, section := range fields {
|
|
|
|
parts := strings.Split(section, ":")
|
|
|
|
if 2 != len(parts) || !strings.HasPrefix(parts[0], "ip") {
|
|
|
|
// ignore unsupported bits
|
|
|
|
// (i.e. +mx +ip include:xxx)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
ip := parts[1]
|
|
|
|
|
|
|
|
if strings.Contains(ip, "/") {
|
|
|
|
_, _, err := net.ParseCIDR(ip)
|
|
|
|
if nil != err {
|
|
|
|
panic(fmt.Errorf("invalid CIDR %q", ip))
|
|
|
|
}
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
|
|
|
|
ipAddr := net.ParseIP(ip)
|
|
|
|
if nil == ipAddr {
|
|
|
|
panic(fmt.Errorf(
|
|
|
|
"IP %q from SPF record could not be parsed",
|
|
|
|
ipAddr.String(),
|
|
|
|
))
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return fields
|
|
|
|
}
|
|
|
|
|
|
|
|
func updateTxt(txtDomain string) error {
|
|
|
|
var newFields []string
|
|
|
|
records, err := net.LookupTXT(txtDomain)
|
|
|
|
if nil != err {
|
|
|
|
return fmt.Errorf("bad spf-domain: %s", err)
|
|
|
|
}
|
|
|
|
for _, record := range records {
|
|
|
|
newFields, err = parseSpf(record)
|
|
|
|
if nil != err {
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
if len(fields) > 0 {
|
|
|
|
break
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
// TODO put a lock here?
|
|
|
|
fields = newFields
|
|
|
|
return nil
|
|
|
|
}
|
|
|
|
|
|
|
|
// IsAllowed returns true if the given IP matches an IP or CIDR in
|
|
|
|
// the whitelist, or if the spf-domain is an empty string explicitly
|
|
|
|
func IsAllowed(remoteIP net.IP) (bool, error) {
|
|
|
|
if !initialized {
|
|
|
|
panic(fmt.Errorf("was not initialized"))
|
|
|
|
}
|
|
|
|
if 0 == len(fields) {
|
|
|
|
return true, nil
|
|
|
|
}
|
2020-08-25 08:08:56 +00:00
|
|
|
if nil == remoteIP {
|
|
|
|
return false, nil
|
|
|
|
}
|
2020-08-25 07:29:10 +00:00
|
|
|
|
|
|
|
for _, section := range fields {
|
|
|
|
parts := strings.Split(section, ":")
|
|
|
|
if 2 != len(parts) || !strings.HasPrefix(parts[0], "ip") {
|
|
|
|
// ignore unsupported bits
|
|
|
|
// (i.e. +mx +ip include:xxx)
|
|
|
|
continue
|
|
|
|
}
|
|
|
|
ip := parts[1]
|
|
|
|
|
|
|
|
if strings.Contains(ip, "/") {
|
|
|
|
_, ipNet, err := net.ParseCIDR(ip)
|
|
|
|
if nil != err {
|
|
|
|
return false, fmt.Errorf("invalid CIDR %q", ip)
|
|
|
|
}
|
|
|
|
return ipNet.Contains(remoteIP), nil
|
|
|
|
}
|
|
|
|
|
|
|
|
ipAddr := net.ParseIP(ip)
|
|
|
|
if nil == ipAddr {
|
|
|
|
return false, fmt.Errorf(
|
|
|
|
"IP %q from SPF record could not be parsed",
|
|
|
|
ipAddr.String(),
|
|
|
|
)
|
|
|
|
}
|
|
|
|
if remoteIP.Equal(ipAddr) {
|
|
|
|
return true, nil
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return false, nil
|
|
|
|
}
|
|
|
|
|
|
|
|
func parseSpf(spf1 string) ([]string, error) {
|
|
|
|
fields := strings.Fields(spf1)
|
|
|
|
if len(fields) < 1 ||
|
|
|
|
len(fields[0]) < 1 ||
|
|
|
|
!strings.HasPrefix(fields[0], "v=") {
|
|
|
|
return nil, errors.New("missing v=")
|
|
|
|
}
|
|
|
|
fields = fields[1:]
|
|
|
|
|
|
|
|
return fields, nil
|
|
|
|
}
|