diff --git a/firewall/dialer.go b/firewall/dialer.go new file mode 100644 index 00000000..41f1aec9 --- /dev/null +++ b/firewall/dialer.go @@ -0,0 +1,44 @@ +package firewall + +import ( + "fmt" + "net" + + "github.com/Safing/portmaster/intel" +) + +func init() { + intel.SetLocalAddrFactory(PermittedAddr) +} + +// PermittedAddr returns an already permitted local address for the given network for reliable connectivity. +// Returns nil in case of error. +func PermittedAddr(network string) net.Addr { + switch network { + case "udp": + return PermittedUDPAddr() + case "tcp": + return PermittedTCPAddr() + } + return nil +} + +// PermittedUDPAddr returns an already permitted local udp address for reliable connectivity. +// Returns nil in case of error. +func PermittedUDPAddr() *net.UDPAddr { + addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf(":%d", GetPermittedPort())) + if err != nil { + return nil + } + return addr +} + +// PermittedTCPAddr returns an already permitted local tcp address for reliable connectivity. +// Returns nil in case of error. +func PermittedTCPAddr() *net.TCPAddr { + addr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf(":%d", GetPermittedPort())) + if err != nil { + return nil + } + return addr +} diff --git a/firewall/firewall.go b/firewall/firewall.go index e956fbe1..715bdc9f 100644 --- a/firewall/firewall.go +++ b/firewall/firewall.go @@ -1,6 +1,7 @@ package firewall import ( + "context" "fmt" "net" "os" @@ -82,6 +83,8 @@ func start() error { // go run() // go run() + go portsInUseCleaner() + return interception.Start() } @@ -91,33 +94,44 @@ func stop() error { func handlePacket(pkt packet.Packet) { - log.Tracef("handling packet: %s", pkt) - - // allow local dns - if pkt.Info().Src.Equal(pkt.Info().Dst) && pkt.Info().DstPort == 53 { - log.Tracef("accepting local dns: %s", pkt) + // allow localhost, for now + if pkt.Info().Src.Equal(pkt.Info().Dst) { + log.Debugf("accepting localhost communication: %s", pkt) pkt.PermanentAccept() return } + // allow local dns + if (pkt.Info().DstPort == 53 || pkt.Info().SrcPort == 53) && pkt.Info().Src.Equal(pkt.Info().Dst) { + log.Debugf("accepting local dns: %s", pkt) + pkt.PermanentAccept() + return + } + + // // redirect dns (if we know that it's not our own request) + // if pkt.IsOutbound() && intel.RemoteIsActiveNameserver(pkt) { + // log.Debugf("redirecting dns: %s", pkt) + // pkt.RedirToNameserver() + // } + // allow ICMP, IGMP and DHCP // TODO: actually handle these switch pkt.Info().Protocol { case packet.ICMP: - log.Tracef("accepting ICMP: %s", pkt) + log.Debugf("accepting ICMP: %s", pkt) pkt.PermanentAccept() return case packet.ICMPv6: - log.Tracef("accepting ICMPv6: %s", pkt) + log.Debugf("accepting ICMPv6: %s", pkt) pkt.PermanentAccept() return case packet.IGMP: - log.Tracef("accepting IGMP: %s", pkt) + log.Debugf("accepting IGMP: %s", pkt) pkt.PermanentAccept() return case packet.UDP: if pkt.Info().DstPort == 67 || pkt.Info().DstPort == 68 { - log.Tracef("accepting DHCP: %s", pkt) + log.Debugf("accepting DHCP: %s", pkt) pkt.PermanentAccept() return } @@ -141,6 +155,9 @@ func handlePacket(pkt packet.Packet) { // } // } + pkt.SetCtx(log.AddTracer(context.Background())) + log.Tracer(pkt.Ctx()).Tracef("firewall: handling packet: %s", pkt) + // associate packet to link and handle link, created := network.GetOrCreateLinkByPacket(pkt) if created { @@ -152,25 +169,50 @@ func handlePacket(pkt packet.Packet) { link.HandlePacket(pkt) return } - verdict(pkt, link.GetVerdict()) - + issueVerdict(pkt, link, 0, true, false) } func initialHandler(pkt packet.Packet, link *network.Link) { + log.Tracer(pkt.Ctx()).Trace("firewall: [initial handler]") + + // check for internal firewall bypass + ps := getPortStatusAndMarkUsed(pkt.Info().LocalPort()) + if ps.isMe { + // connect to comms + comm, err := network.GetOwnComm(pkt) + if err != nil { + // log.Warningf("firewall: could not get own comm: %s", err) + log.Tracer(pkt.Ctx()).Warningf("firewall: could not get own comm: %s", err) + } else { + comm.AddLink(link) + } + + // approve + link.Accept("internally approved") + log.Tracer(pkt.Ctx()).Tracef("firewall: internally approved link (via local port %d)", pkt.Info().LocalPort()) + + // finish + link.StopFirewallHandler() + issueVerdict(pkt, link, 0, true, true) + + return + } // get Communication comm, err := network.GetCommunicationByFirstPacket(pkt) if err != nil { + log.Tracer(pkt.Ctx()).Warningf("firewall: could not get process, denying link: %s", err) + // get "unknown" comm link.Deny(fmt.Sprintf("could not get process: %s", err)) comm, err = network.GetUnknownCommunication(pkt) if err != nil { // all failed - log.Errorf("firewall: could not get unknown comm (dropping %s): %s", pkt.String(), err) + log.Tracer(pkt.Ctx()).Errorf("firewall: could not get unknown comm: %s", err) link.UpdateVerdict(network.VerdictDrop) - verdict(pkt, network.VerdictDrop) link.StopFirewallHandler() + issueVerdict(pkt, link, 0, true, true) return } @@ -178,24 +220,20 @@ func initialHandler(pkt packet.Packet, link *network.Link) { // add new Link to Communication (and save both) comm.AddLink(link) - - log.Tracef("comm [%s] has new link [%s]", comm, link) + log.Tracer(pkt.Ctx()).Tracef("firewall: link attached to %s", comm) // reroute dns requests to nameserver if comm.Process().Pid != os.Getpid() && pkt.IsOutbound() && pkt.Info().DstPort == 53 && !pkt.Info().Src.Equal(pkt.Info().Dst) { - log.Tracef("redirecting [%s] to nameserver", link) - link.RerouteToNameserver() - verdict(pkt, link.GetVerdict()) + link.UpdateVerdict(network.VerdictRerouteToNameserver) link.StopFirewallHandler() + issueVerdict(pkt, link, 0, true, true) return } + log.Tracer(pkt.Ctx()).Trace("firewall: starting decision process") DecideOnCommunication(comm, pkt) DecideOnLink(comm, link, pkt) - // log decision - logInitialVerdict(link) - // TODO: link this to real status // gate17Active := mode.Client() @@ -215,7 +253,7 @@ func initialHandler(pkt packet.Packet, link *network.Link) { inspectThenVerdict(pkt, link) default: link.StopFirewallHandler() - verdict(pkt, link.GetVerdict()) + issueVerdict(pkt, link, 0, true, false) } } @@ -223,76 +261,70 @@ func initialHandler(pkt packet.Packet, link *network.Link) { func inspectThenVerdict(pkt packet.Packet, link *network.Link) { pktVerdict, continueInspection := inspection.RunInspectors(pkt, link) if continueInspection { - // do not allow to circumvent link decision: e.g. to ACCEPT packets from a DROP-ed link - linkVerdict := link.GetVerdict() - if pktVerdict > linkVerdict { - verdict(pkt, pktVerdict) - } else { - verdict(pkt, linkVerdict) - } + issueVerdict(pkt, link, pktVerdict, false, false) return } // we are done with inspecting link.StopFirewallHandler() - - link.Lock() - defer link.Unlock() - link.VerdictPermanent = permanentVerdicts() - if link.VerdictPermanent { - go link.Save() - permanentVerdict(pkt, link.Verdict) - } else { - verdict(pkt, link.Verdict) - } + issueVerdict(pkt, link, 0, true, false) } -func permanentVerdict(pkt packet.Packet, action network.Verdict) { - switch action { +func issueVerdict(pkt packet.Packet, link *network.Link, verdict network.Verdict, allowPermanent, forceSave bool) { + link.Lock() + + // enable permanent verdict + if allowPermanent && !link.VerdictPermanent { + link.VerdictPermanent = permanentVerdicts() + if link.VerdictPermanent { + forceSave = true + } + } + + // do not allow to circumvent link decision: e.g. to ACCEPT packets from a DROP-ed link + if verdict < link.Verdict { + verdict = link.Verdict + } + + switch verdict { case network.VerdictAccept: atomic.AddUint64(packetsAccepted, 1) - pkt.PermanentAccept() - return + if link.VerdictPermanent { + pkt.PermanentAccept() + } else { + pkt.Accept() + } case network.VerdictBlock: atomic.AddUint64(packetsBlocked, 1) - pkt.PermanentBlock() - return + if link.VerdictPermanent { + pkt.PermanentBlock() + } else { + pkt.Block() + } case network.VerdictDrop: atomic.AddUint64(packetsDropped, 1) - pkt.PermanentDrop() - return + if link.VerdictPermanent { + pkt.PermanentDrop() + } else { + pkt.Drop() + } case network.VerdictRerouteToNameserver: pkt.RerouteToNameserver() - return case network.VerdictRerouteToTunnel: pkt.RerouteToTunnel() - return - } - pkt.Drop() -} - -func verdict(pkt packet.Packet, action network.Verdict) { - switch action { - case network.VerdictAccept: - atomic.AddUint64(packetsAccepted, 1) - pkt.Accept() - return - case network.VerdictBlock: - atomic.AddUint64(packetsBlocked, 1) - pkt.Block() - return - case network.VerdictDrop: + default: atomic.AddUint64(packetsDropped, 1) pkt.Drop() - return - case network.VerdictRerouteToNameserver: - pkt.RerouteToNameserver() - return - case network.VerdictRerouteToTunnel: - pkt.RerouteToTunnel() - return } - pkt.Drop() + + link.Unlock() + + log.InfoTracef(pkt.Ctx(), "firewall: %s %s", link.Verdict, link) + + if forceSave && !link.KeyIsSet() { + // always save if not yet saved + go link.Save() + } } // func tunnelHandler(pkt packet.Packet) { @@ -308,32 +340,6 @@ func verdict(pkt packet.Packet, action network.Verdict) { // return // } -func logInitialVerdict(link *network.Link) { - // switch link.GetVerdict() { - // case network.VerdictAccept: - // log.Infof("firewall: accepting new link: %s", link.String()) - // case network.VerdictBlock: - // log.Infof("firewall: blocking new link: %s", link.String()) - // case network.VerdictDrop: - // log.Infof("firewall: dropping new link: %s", link.String()) - // case network.VerdictRerouteToNameserver: - // log.Infof("firewall: rerouting new link to nameserver: %s", link.String()) - // case network.VerdictRerouteToTunnel: - // log.Infof("firewall: rerouting new link to tunnel: %s", link.String()) - // } -} - -func logChangedVerdict(link *network.Link) { - // switch link.GetVerdict() { - // case network.VerdictAccept: - // log.Infof("firewall: change! - now accepting link: %s", link.String()) - // case network.VerdictBlock: - // log.Infof("firewall: change! - now blocking link: %s", link.String()) - // case network.VerdictDrop: - // log.Infof("firewall: change! - now dropping link: %s", link.String()) - // } -} - func run() { for { select { diff --git a/firewall/master.go b/firewall/master.go index d2625886..17fee652 100644 --- a/firewall/master.go +++ b/firewall/master.go @@ -13,6 +13,7 @@ import ( "github.com/Safing/portmaster/network" "github.com/Safing/portmaster/network/netutils" "github.com/Safing/portmaster/network/packet" + "github.com/Safing/portmaster/process" "github.com/Safing/portmaster/profile" "github.com/Safing/portmaster/status" "github.com/miekg/dns" @@ -463,6 +464,40 @@ func DecideOnCommunication(comm *network.Communication, pkt packet.Packet) { // DecideOnLink makes a decision about a link with the first packet. func DecideOnLink(comm *network.Communication, link *network.Link, pkt packet.Packet) { + // grant self + if comm.Process().Pid == os.Getpid() { + log.Infof("firewall: granting own link %s", comm) + link.Accept("") + return + } + + // check if communicating with self + if comm.Process().Pid >= 0 && pkt.Info().Src.Equal(pkt.Info().Dst) { + // get PID + otherPid, _, err := process.GetPidByEndpoints( + pkt.Info().RemoteIP(), + pkt.Info().RemotePort(), + pkt.Info().LocalIP(), + pkt.Info().LocalPort(), + pkt.Info().Protocol, + ) + if err == nil { + + // get primary process + otherProcess, err := process.GetOrFindPrimaryProcess(pkt.Ctx(), otherPid) + if err == nil { + + if otherProcess.Pid == comm.Process().Pid { + log.Infof("firewall: permitting connection to self %s", comm) + link.Accept("connection to self") + return + } + + } + } + } + + // check if we aleady have a verdict switch comm.GetVerdict() { case network.VerdictUndecided, network.VerdictUndeterminable: // continue @@ -471,13 +506,6 @@ func DecideOnLink(comm *network.Communication, link *network.Link, pkt packet.Pa return } - // grant self - if comm.Process().Pid == os.Getpid() { - log.Infof("firewall: granting own link %s", comm) - link.Accept("") - return - } - // check if there is a profile profileSet := comm.Process().ProfileSet() if profileSet == nil { diff --git a/firewall/ports.go b/firewall/ports.go new file mode 100644 index 00000000..9401532c --- /dev/null +++ b/firewall/ports.go @@ -0,0 +1,90 @@ +package firewall + +import ( + "sync" + "time" + + "github.com/Safing/portbase/crypto/random" + "github.com/Safing/portbase/log" +) + +type portStatus struct { + lastSeen time.Time + isMe bool +} + +var ( + portsInUse = make(map[uint16]*portStatus) + portsInUseLock sync.Mutex + + cleanerTickDuration = 10 * time.Second + cleanTimeout = 10 * time.Minute +) + +func getPortStatusAndMarkUsed(port uint16) *portStatus { + portsInUseLock.Lock() + defer portsInUseLock.Unlock() + + ps, ok := portsInUse[port] + if ok { + ps.lastSeen = time.Now() + return ps + } + + new := &portStatus{ + lastSeen: time.Now(), + isMe: false, + } + portsInUse[port] = new + return new +} + +// GetPermittedPort returns a local port number that is already permitted for communication. +// This bypasses the process attribution step to guarantee connectivity. +// Communication on the returned port is attributed to the Portmaster. +func GetPermittedPort() uint16 { + portsInUseLock.Lock() + defer portsInUseLock.Unlock() + + for i := 0; i < 1000; i++ { + // generate port between 10000 and 65535 + rN, err := random.Number(55535) + if err != nil { + log.Warningf("firewall: failed to generate random port: %s", err) + return 0 + } + port := uint16(rN + 10000) + + // check if free, return if it is + _, ok := portsInUse[port] + if !ok { + portsInUse[port] = &portStatus{ + lastSeen: time.Now(), + isMe: true, + } + return port + } + } + + return 0 +} + +func portsInUseCleaner() { + for { + time.Sleep(cleanerTickDuration) + cleanPortsInUse() + } +} + +func cleanPortsInUse() { + portsInUseLock.Lock() + defer portsInUseLock.Unlock() + + threshhold := time.Now().Add(-cleanTimeout) + + for port, status := range portsInUse { + if status.lastSeen.Before(threshhold) { + delete(portsInUse, port) + } + } +}