You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
fw-daemon/sgfw/rules.go

546 lines
13 KiB

package sgfw
import (
// "encoding/binary"
"fmt"
"io/ioutil"
"net"
"os"
"path"
"regexp"
"strconv"
"strings"
nfqueue "github.com/subgraph/go-nfnetlink/nfqueue"
// "github.com/subgraph/go-nfnetlink"
"github.com/subgraph/go-procsnitch"
)
const matchAny = 0
//const noAddress = uint32(0xffffffff)
var anyAddress net.IP = net.IP{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0}
var noAddress net.IP = net.IP{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}
type Rule struct {
id uint
policy *Policy
mode RuleMode
rtype RuleAction
proto string
pid int
hostname string
network *net.IPNet
addr net.IP
saddr net.IP
port uint16
uid int
gid int
uname string
gname string
sandbox string
}
func (r *Rule) String() string {
return r.getString(false)
}
func (r *Rule) getString(redact bool) string {
rtype := RuleActionString[RULE_ACTION_DENY]
if r.rtype == RULE_ACTION_ALLOW || r.rtype == RULE_ACTION_ALLOW_TLSONLY {
rtype = RuleActionString[r.rtype]
}
rmode := "|" + RuleModeString[r.mode]
protostr := ""
if r.proto != "tcp" {
protostr = r.proto + ":"
}
rpriv := fmt.Sprintf("|%d:%d", r.uid, r.gid)
sbox := "|"
if r.sandbox != "" {
sbox = "|" + sbox
}
return fmt.Sprintf("%s|%s%s%s%s%s", rtype, protostr, r.AddrString(redact), rmode, rpriv, sbox)
}
func (r *Rule) AddrString(redact bool) string {
addr := "*"
port := "*"
if r.hostname != "" {
addr = r.hostname
} else if r.network != nil {
addr = r.network.String()
} else if !addrMatchesAny(r.addr) && !addrMatchesNone(r.addr) {
// bs := make([]byte, 4)
// binary.BigEndian.PutUint32(bs, r.addr)
// addr = fmt.Sprintf("%d.%d.%d.%d", bs[0], bs[1], bs[2], bs[3])
addr = r.addr.String()
}
if r.port != matchAny || r.proto == "icmp" {
port = fmt.Sprintf("%d", r.port)
}
if redact && addr != "*" {
addr = STR_REDACTED
}
return fmt.Sprintf("%s:%s", addr, port)
}
type RuleList []*Rule
func (r *Rule) match(src net.IP, dst net.IP, dstPort uint16, hostname string, proto string, uid, gid int, uname, gname string, sandbox string) bool {
if r.policy.sandbox != sandbox {
return false
}
if r.proto != proto {
return false
}
if r.uid != -1 && r.uid != uid {
return false
} else if r.gid != -1 && r.gid != gid {
return false
} else if r.uname != "" && r.uname != uname {
return false
} else if r.gname != "" && r.gname != gname {
return false
}
// log.Notice("comparison: ", hostname, " / ", dst, " : ", dstPort, " -> ", r.addr, " / ", r.hostname, " : ", r.port)
if r.port != matchAny && r.port != dstPort {
return false
}
if addrMatchesAny(r.addr) {
return true
}
if r.hostname != "" {
if strings.ContainsAny(r.hostname, "*") {
regstr := strings.Replace(r.hostname, "*", ".?", -1)
match, err := regexp.MatchString(regstr, hostname)
if err != nil {
log.Errorf("Error comparing hostname against mask %s: %v", regstr, err)
} else {
return match
}
}
return r.hostname == hostname
}
if r.network != nil && r.network.Contains(dst) {
return true
}
if proto == "icmp" {
//fmt.Printf("network = %v, src = %v, r.addr = %x, src to4 = %x\n", r.network, src, r.addr, binary.BigEndian.Uint32(src.To4()))
if (r.network != nil && r.network.Contains(src)) || (r.addr.Equal(src)) {
return true
}
}
return r.addr.Equal(dst)
}
func (rl *RuleList) filterPacket(p *nfqueue.NFQPacket, pinfo *procsnitch.Info, srcip net.IP, hostname, optstr string) FilterResult {
_, dstip := getPacketIPAddrs(p)
_, dstp := getPacketPorts(p)
return rl.filter(p, srcip, dstip, dstp, hostname, pinfo, optstr)
}
func (rl *RuleList) filter(pkt *nfqueue.NFQPacket, src, dst net.IP, dstPort uint16, hostname string, pinfo *procsnitch.Info, optstr string) FilterResult {
if rl == nil {
return FILTER_PROMPT
}
result := FILTER_PROMPT
sandboxed := false
if pinfo.Sandbox != "" {
sandboxed = true
}
// sandboxed := strings.HasPrefix(optstr, "SOCKS5|Tor / Sandbox")
for _, r := range *rl {
nfqproto := ""
//log.Notice("------------ trying match of src ", src, " against: ", r, " | ", r.saddr, " / optstr = ", optstr, "; pid ", pinfo.Pid, " vs rule pid ", r.pid)
//log.Notice("r.saddr: ", r.saddr, "src: ", src, "sandboxed ", sandboxed, "optstr: ", optstr)
if r.saddr == nil && src != nil && sandboxed {
log.Notice("! Skipping comparison against incompatible rule types: rule src = ", r.saddr, " / packet src = ", src)
// continue
} else if r.saddr == nil && src == nil && sandboxed {
// continue
// r.match(src, dst, dstPort, hostname, nil, pinfo.UID, pinfo.GID, uidToUser(pinfo.UID), gidToGroup(pinfo.GID))
nfqproto = "tcp"
} else if r.saddr != nil && !r.saddr.Equal(src) && r.proto != "icmp" {
log.Notice("! Skipping comparison of mismatching source ips")
continue
} else {
if pkt != nil {
nfqproto = getNFQProto(pkt)
} else {
if r.saddr == nil && src == nil && sandboxed == false && (r.port == dstPort || r.port == matchAny) && (r.addr.Equal(anyAddress) || r.hostname == "" || r.hostname == hostname) {
// log.Notice("+ Socks5 MATCH SUCCEEDED")
if r.rtype == RULE_ACTION_DENY {
return FILTER_DENY
} else if r.rtype == RULE_ACTION_ALLOW {
return FILTER_ALLOW
} else if r.rtype == RULE_ACTION_ALLOW_TLSONLY {
return FILTER_ALLOW_TLSONLY
}
} else {
return FILTER_PROMPT
}
}
}
// log.Notice("r.saddr = ", r.saddr, "src = ", src, "\n")
if r.pid >= 0 && r.pid != pinfo.Pid {
//log.Notice("! Skipping comparison of mismatching PIDs")
continue
}
if r.match(src, dst, dstPort, hostname, nfqproto, pinfo.UID, pinfo.GID, uidToUser(pinfo.UID), gidToGroup(pinfo.GID), pinfo.Sandbox) {
// log.Notice("+ MATCH SUCCEEDED")
dstStr := dst.String()
if FirewallConfig.LogRedact {
dstStr = STR_REDACTED
}
srcStr := STR_UNKNOWN
if pkt != nil {
srcip, _ := getPacketIPAddrs(pkt)
srcp, _ := getPacketPorts(pkt)
srcStr = fmt.Sprintf("%s:%d", srcip, srcp)
}
// log.Noticef("%s > %s %s %s -> %s:%d",
//r.getString(FirewallConfig.LogRedact), pinfo.ExePath, r.proto, srcStr, dstStr, dstPort)
if r.rtype == RULE_ACTION_DENY {
//TODO: Optionally redact below log entry
log.Warningf("DENIED outgoing connection attempt by %s from %s %s -> %s:%d",
pinfo.ExePath, r.proto,
srcStr,
dstStr, dstPort)
return FILTER_DENY
} else if r.rtype == RULE_ACTION_ALLOW {
result = FILTER_ALLOW
return result
/*
if r.saddr != nil {
return result
}
*/
} else if r.rtype == RULE_ACTION_ALLOW_TLSONLY {
result = FILTER_ALLOW_TLSONLY
return result
}
}
/**else {
log.Notice("+ MATCH FAILED")
} */
}
// log.Notice("--- RESULT = ", result)
return result
}
func parseError(s string) error {
return fmt.Errorf("unable to parse rule string: %s", s)
}
func (r *Rule) parse(s string) bool {
r.addr = noAddress
r.saddr = nil
parts := strings.Split(s, "|")
if len(parts) < 4 || len(parts) > 6 {
log.Notice("invalid number ", len(parts), " of rule parts in line ", s)
return false
}
if parts[2] == "SYSTEM" {
r.mode = RULE_MODE_SYSTEM
} else if parts[2] == "PERMANENT" {
r.mode = RULE_MODE_PERMANENT
} else if parts[2] != "" {
log.Warning("Error: invalid rule mode ", parts[2], " in line ", s)
return false
}
if !r.parsePrivs(parts[3]) {
log.Warning("Error: invalid privs ", parts[3], " in line ", s)
return false
}
if !r.parseSandbox(parts[4]) {
log.Warning("invalid sandbox ", parts[4], "in line ", s)
return false
}
// fmt.Printf("uid = %v, gid = %v, user = %v, group = %v, hostname = %v, sandbox = %v\n", r.uid, r.gid, r.uname, r.gname, r.hostname, r.sandbox)
if len(parts) == 6 && len(strings.TrimSpace(parts[5])) > 0 {
r.saddr = net.ParseIP(parts[5])
if r.saddr == nil {
log.Notice("invalid source IP ", parts[5], " in line ", s)
return false
}
}
return r.parseVerb(parts[0]) && r.parseTarget(parts[1])
}
func (r *Rule) parseSandbox(p string) bool {
r.sandbox = p
return true
}
func (r *Rule) parsePrivs(p string) bool {
toks := strings.Split(p, ":")
if len(toks) > 2 {
return false
}
r.uid, r.gid = -1, -1
r.uname, r.gname = "", ""
ustr := toks[0]
uid, err := strconv.Atoi(ustr)
if err != nil {
r.uname = ustr
} else {
r.uid = uid
}
if len(toks) > 1 {
gstr := toks[1]
gid, err := strconv.Atoi(gstr)
if err != nil {
r.gname = gstr
} else {
r.gid = gid
}
}
return true
}
func (r *Rule) parseVerb(v string) bool {
switch v {
case RuleActionString[RULE_ACTION_ALLOW]:
r.rtype = RULE_ACTION_ALLOW
return true
case RuleActionString[RULE_ACTION_ALLOW_TLSONLY]:
r.rtype = RULE_ACTION_ALLOW_TLSONLY
return true
case RuleActionString[RULE_ACTION_DENY]:
r.rtype = RULE_ACTION_DENY
return true
}
return false
}
func (r *Rule) parseTarget(t string) bool {
addrPort := strings.Split(t, ":")
if len(addrPort) < 2 {
return false
}
sind := 0
lind := len(addrPort) - 1
if addrPort[0] == "udp" || addrPort[0] == "icmp" || addrPort[0] == "tcp" {
r.proto = addrPort[0]
sind++
} else {
r.proto = "tcp"
}
newAddr := strings.Join(addrPort[sind:lind], ":")
return r.parseAddr(newAddr) && r.parsePort(addrPort[lind])
// return r.parseAddr(addrPort[sind]) && r.parsePort(addrPort[sind+1])
}
func (r *Rule) parseAddr(a string) bool {
if a == "*" {
r.hostname = ""
r.addr = anyAddress
return true
}
// if strings.IndexFunc(a, unicode.IsLetter) != -1 {
if net.ParseIP(a) == nil {
r.hostname = a
return true
}
// ip := net.ParseIP(a)
ip, ipnet, err := net.ParseCIDR(a)
if err != nil || ip == nil {
ip = net.ParseIP(a)
if ip == nil {
return false
}
} else {
r.network = ipnet
}
r.addr = ip
return true
}
func (r *Rule) parsePort(p string) bool {
if p == "*" {
r.port = matchAny
return true
}
var err error
port, err := strconv.ParseUint(p, 10, 16)
if err != nil || (port == 0 && r.proto != "icmp") || port > 0xFFFF {
return false
}
r.port = uint16(port)
return true
}
const ruleFile = "/var/lib/sgfw/sgfw_rules"
func maybeCreateDir(dir string) error {
_, err := os.Stat(dir)
if os.IsNotExist(err) {
return os.MkdirAll(dir, 0755)
}
return err
}
func rulesPath() (string, error) {
if err := maybeCreateDir(path.Dir(ruleFile)); err != nil {
return ruleFile, err
}
return ruleFile, nil
}
func (fw *Firewall) saveRules() {
fw.lock.Lock()
defer fw.lock.Unlock()
p, err := rulesPath()
if err != nil {
log.Warningf("Failed to open %s for writing: %v", p, err)
return
}
f, err := os.Create(p)
if err != nil {
log.Warningf("Failed to open %s for writing: %v", p, err)
return
}
defer f.Close()
for _, p := range fw.policies {
savePolicy(f, p)
}
}
func savePolicy(f *os.File, p *Policy) {
p.lock.Lock()
defer p.lock.Unlock()
if !p.hasPersistentRules() {
return
}
log.Warningf("p.path: ", p.path)
if !writeLine(f, "["+p.sandbox+"|"+p.path+"]") {
return
}
for _, r := range p.rules {
if r.mode == RULE_MODE_PERMANENT || r.mode == RULE_MODE_SYSTEM {
if !writeLine(f, r.String()) {
return
}
}
}
}
func writeLine(f *os.File, line string) bool {
_, err := f.WriteString(line + "\n")
if err != nil {
log.Warningf("Error writing to rule file: %v", err)
return false
}
return true
}
func (fw *Firewall) loadRules() {
fw.lock.Lock()
defer fw.lock.Unlock()
fw.clearRules()
p, err := rulesPath()
if err != nil {
log.Warningf("Failed to open %s for reading: %v", p, err)
return
}
bs, err := ioutil.ReadFile(p)
if err != nil {
if !os.IsNotExist(err) {
log.Warningf("Failed to open %s for reading: %v", p, err)
} else {
log.Warningf("Did not find a rules file at %s: SGFW loaded with no rules\n", p)
}
return
}
var policy *Policy
for _, line := range strings.Split(string(bs), "\n") {
if strings.HasPrefix(line, "[") && strings.HasSuffix(line, "]") {
policy = fw.processPathLine(line)
} else {
trimmed := strings.TrimSpace(line)
if len(trimmed) > 0 && trimmed[:1] != "#" {
processRuleLine(policy, trimmed)
}
}
}
}
func (fw *Firewall) processPathLine(line string) *Policy {
pathLine := line[1 : len(line)-1]
toks := strings.Split(pathLine, "|")
if len(toks) != 2 {
log.Warning("Error parsing rules directive:", line)
return nil
}
policy := fw.policyForPathAndSandbox(toks[1], toks[0])
policy.lock.Lock()
defer policy.lock.Unlock()
policy.rules = nil
return policy
}
func processRuleLine(policy *Policy, line string) {
if policy == nil {
log.Warningf("Cannot process rule line without first seeing policy key line: %s", line)
return
}
_, err := policy.parseRule(line, true)
if err != nil {
log.Warningf("Error parsing rule (%s): %v", line, err)
return
}
}
func addrMatchesAny(addr net.IP) bool {
any := anyAddress
if addr.To4() != nil {
any = net.IP{0, 0, 0, 0}
}
return any.Equal(addr)
}
func addrMatchesNone(addr net.IP) bool {
none := noAddress
if addr.To4() != nil {
none = net.IP{0xff, 0xff, 0xff, 0xff}
}
return none.Equal(addr)
}