telebit/internal/iplist/iplist.go

142 lines
2.8 KiB
Go

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(newFields) > 0 {
break
}
return fmt.Errorf("no spf records found")
}
// 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
}
if nil == remoteIP {
return false, nil
}
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
}