Merge pull request #285 from safing/fix/patchset-2

Fix early connection handling and mimetypes
This commit is contained in:
Daniel 2021-04-07 16:46:28 +02:00 committed by GitHub
commit f44fa91d5e
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
6 changed files with 286 additions and 157 deletions

View file

@ -1,46 +0,0 @@
package firewall
import (
"fmt"
"net"
"github.com/safing/portmaster/netenv"
"github.com/safing/portmaster/resolver"
)
func init() {
resolver.SetLocalAddrFactory(PermittedAddr)
netenv.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
}

View file

@ -3,12 +3,14 @@ package firewall
import ( import (
"context" "context"
"errors" "errors"
"fmt"
"net" "net"
"os" "os"
"sync/atomic" "sync/atomic"
"time" "time"
"github.com/safing/portmaster/netenv" "github.com/safing/portmaster/netenv"
"golang.org/x/sync/singleflight"
"github.com/tevino/abool" "github.com/tevino/abool"
@ -63,7 +65,6 @@ func interceptionStart() error {
interceptionModule.StartWorker("stat logger", statLogger) interceptionModule.StartWorker("stat logger", statLogger)
interceptionModule.StartWorker("packet handler", packetHandler) interceptionModule.StartWorker("packet handler", packetHandler)
interceptionModule.StartWorker("ports state cleaner", portsInUseCleaner)
return interception.Start() return interception.Start()
} }
@ -103,20 +104,60 @@ func handlePacket(ctx context.Context, pkt packet.Packet) {
} }
pkt.SetCtx(traceCtx) pkt.SetCtx(traceCtx)
// associate packet to link and handle // Get connection of packet.
conn, ok := network.GetConnection(pkt.GetConnectionID()) conn, err := getConnection(pkt)
if ok { if err != nil {
tracer.Tracef("filter: assigned to connection %s", conn.ID) tracer.Errorf("filter: packet %s dropped: %s", pkt, err)
} else { _ = pkt.Drop()
conn = network.NewConnectionFromFirstPacket(pkt) return
tracer.Tracef("filter: created new connection %s", conn.ID)
conn.SetFirewallHandler(initialHandler)
} }
// handle packet // handle packet
conn.HandlePacket(pkt) conn.HandlePacket(pkt)
} }
var getConnectionSingleInflight singleflight.Group
func getConnection(pkt packet.Packet) (*network.Connection, error) {
created := false
// Create or get connection in single inflight lock in order to prevent duplicates.
newConn, err, shared := getConnectionSingleInflight.Do(pkt.GetConnectionID(), func() (interface{}, error) {
// First, check for an existing connection.
conn, ok := network.GetConnection(pkt.GetConnectionID())
if ok {
return conn, nil
}
// Else create new one from the packet.
conn = network.NewConnectionFromFirstPacket(pkt)
conn.SetFirewallHandler(initialHandler)
created = true
return conn, nil
})
if err != nil {
return nil, fmt.Errorf("failed to get connection: %s", err)
}
if newConn == nil {
return nil, errors.New("connection getter returned nil")
}
// Transform and log result.
conn := newConn.(*network.Connection)
switch {
case created && shared:
log.Tracer(pkt.Ctx()).Tracef("filter: created new connection %s (shared)", conn.ID)
case created:
log.Tracer(pkt.Ctx()).Tracef("filter: created new connection %s", conn.ID)
case shared:
log.Tracer(pkt.Ctx()).Tracef("filter: assigned connection %s (shared)", conn.ID)
default:
log.Tracer(pkt.Ctx()).Tracef("filter: assigned connection %s", conn.ID)
}
return conn, nil
}
// fastTrackedPermit quickly permits certain network criticial or internal connections. // fastTrackedPermit quickly permits certain network criticial or internal connections.
func fastTrackedPermit(pkt packet.Packet) (handled bool) { func fastTrackedPermit(pkt packet.Packet) (handled bool) {
meta := pkt.Info() meta := pkt.Info()
@ -265,13 +306,12 @@ func fastTrackedPermit(pkt packet.Packet) (handled bool) {
func initialHandler(conn *network.Connection, pkt packet.Packet) { func initialHandler(conn *network.Connection, pkt packet.Packet) {
log.Tracer(pkt.Ctx()).Trace("filter: handing over to connection-based handler") log.Tracer(pkt.Ctx()).Trace("filter: handing over to connection-based handler")
// check for internal firewall bypass // Check for pre-authenticated port.
ps := getPortStatusAndMarkUsed(pkt.Info().LocalPort()) if localPortIsPreAuthenticated(conn.Entity.Protocol, conn.LocalPort) {
if ps.isMe { // Approve connection.
// approve
conn.Accept("connection by Portmaster", noReasonOptionKey) conn.Accept("connection by Portmaster", noReasonOptionKey)
conn.Internal = true conn.Internal = true
// finish // Finalize connection.
conn.StopFirewallHandler() conn.StopFirewallHandler()
issueVerdict(conn, pkt, 0, true) issueVerdict(conn, pkt, 0, true)
return return

View file

@ -1,95 +0,0 @@
package firewall
import (
"context"
"sync"
"time"
"github.com/safing/portbase/log"
"github.com/safing/portbase/rng"
)
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 := rng.Number(55535)
if err != nil {
log.Warningf("filter: 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(ctx context.Context) error {
for {
select {
case <-ctx.Done():
return nil
case <-time.After(cleanerTickDuration):
cleanPortsInUse()
}
}
}
func cleanPortsInUse() {
portsInUseLock.Lock()
defer portsInUseLock.Unlock()
threshold := time.Now().Add(-cleanTimeout)
for port, status := range portsInUse {
if status.lastSeen.Before(threshold) {
delete(portsInUse, port)
}
}
}

113
firewall/preauth.go Normal file
View file

@ -0,0 +1,113 @@
package firewall
import (
"strconv"
"sync"
"github.com/safing/portmaster/network"
"github.com/safing/portmaster/network/packet"
"fmt"
"net"
"github.com/safing/portmaster/netenv"
"github.com/safing/portmaster/resolver"
)
var (
preAuthenticatedPorts = make(map[string]struct{})
preAuthenticatedPortsLock sync.Mutex
)
func init() {
resolver.SetLocalAddrFactory(PermittedAddr)
netenv.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 {
preAuthdPort := GetPermittedPort(packet.UDP)
if preAuthdPort == 0 {
return nil
}
addr, err := net.ResolveUDPAddr("udp", fmt.Sprintf(":%d", preAuthdPort))
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 {
preAuthdPort := GetPermittedPort(packet.TCP)
if preAuthdPort == 0 {
return nil
}
addr, err := net.ResolveTCPAddr("tcp", fmt.Sprintf(":%d", preAuthdPort))
if err != nil {
return nil
}
return addr
}
// 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.
// Every pre-authenticated port is only valid once.
// If no unused local port number can be found, it will return 0, which is
// expected to trigger automatic port selection by the underlying OS.
func GetPermittedPort(protocol packet.IPProtocol) uint16 {
port, ok := network.GetUnusedLocalPort(uint8(protocol))
if !ok {
return 0
}
preAuthenticatedPortsLock.Lock()
defer preAuthenticatedPortsLock.Unlock()
// Save generated port.
key := generateLocalPreAuthKey(uint8(protocol), port)
preAuthenticatedPorts[key] = struct{}{}
return port
}
// localPortIsPreAuthenticated checks if the given protocol and port are
// pre-authenticated and should be attributed to the Portmaster itself.
func localPortIsPreAuthenticated(protocol uint8, port uint16) bool {
preAuthenticatedPortsLock.Lock()
defer preAuthenticatedPortsLock.Unlock()
// Check if the given protocol and port are pre-authenticated.
key := generateLocalPreAuthKey(protocol, port)
_, ok := preAuthenticatedPorts[key]
if ok {
// Immediately remove pre authenticated port.
delete(preAuthenticatedPorts, key)
}
return ok
}
// generateLocalPreAuthKey creates a map key for the pre-authenticated ports.
func generateLocalPreAuthKey(protocol uint8, port uint16) string {
return strconv.Itoa(int(protocol)) + ":" + strconv.Itoa(int(port))
}

49
network/ports.go Normal file
View file

@ -0,0 +1,49 @@
package network
import (
"github.com/safing/portbase/log"
"github.com/safing/portbase/rng"
)
// GetUnusedLocalPort returns a local port of the specified protocol that is
// currently unused and is unlikely to be used within the next seconds.
func GetUnusedLocalPort(protocol uint8) (port uint16, ok bool) {
allConns := conns.clone()
tries := 1000
hundredth := tries / 100
// Try up to 1000 times to find an unused port.
nextPort:
for i := 0; i < tries; i++ {
// Generate random port between 10000 and 65535
rN, err := rng.Number(55535)
if err != nil {
log.Warningf("network: failed to generate random port: %s", err)
return 0, false
}
port := uint16(rN + 10000)
// Shrink range when we chew through the tries.
portRangeStart := port - uint16(100-(i/hundredth))
// Check if the generated port is unused.
nextConnection:
for _, conn := range allConns {
// Skip connection if the protocol does not match the protocol of interest.
if conn.Entity.Protocol != protocol {
continue nextConnection
}
// Skip port if the local port is in dangerous proximity.
// Consecutive port numbers are very common.
if conn.LocalPort <= port && conn.LocalPort >= portRangeStart {
continue nextPort
}
}
// The checks have passed. We have found a good unused port.
return port, true
}
return 0, false
}

View file

@ -3,7 +3,6 @@ package ui
import ( import (
"fmt" "fmt"
"io" "io"
"mime"
"net/http" "net/http"
"net/url" "net/url"
"path/filepath" "path/filepath"
@ -143,7 +142,7 @@ func ServeFileFromBundle(w http.ResponseWriter, r *http.Request, bundleName stri
// set content type // set content type
_, ok := w.Header()["Content-Type"] _, ok := w.Header()["Content-Type"]
if !ok { if !ok {
contentType := mime.TypeByExtension(filepath.Ext(path)) contentType := mimeTypeByExtension(filepath.Ext(path))
if contentType != "" { if contentType != "" {
w.Header().Set("Content-Type", contentType) w.Header().Set("Content-Type", contentType)
} }
@ -179,3 +178,72 @@ func redirectToDefault(w http.ResponseWriter, r *http.Request) {
func redirAddSlash(w http.ResponseWriter, r *http.Request) { func redirAddSlash(w http.ResponseWriter, r *http.Request) {
http.Redirect(w, r, r.RequestURI+"/", http.StatusPermanentRedirect) http.Redirect(w, r, r.RequestURI+"/", http.StatusPermanentRedirect)
} }
// We now do our mimetypes ourselves, because, as far as we analyzed, a Windows
// update screwed us over here and broke all the mime typing.
// (April 2021)
var (
defaultMimeType = "application/octet-stream"
mimeTypes = map[string]string{
".7z": "application/x-7z-compressed",
".atom": "application/atom+xml",
".css": "text/css; charset=utf-8",
".csv": "text/csv; charset=utf-8",
".deb": "application/x-debian-package",
".epub": "application/epub+zip",
".es": "application/ecmascript",
".flv": "video/x-flv",
".gif": "image/gif",
".gz": "application/gzip",
".htm": "text/html; charset=utf-8",
".html": "text/html; charset=utf-8",
".jpeg": "image/jpeg",
".jpg": "image/jpeg",
".js": "text/javascript; charset=utf-8",
".json": "application/json",
".m3u": "audio/mpegurl",
".m4a": "audio/mpeg",
".md": "text/markdown; charset=utf-8",
".mjs": "text/javascript; charset=utf-8",
".mov": "video/quicktime",
".mp3": "audio/mpeg",
".mp4": "video/mp4",
".mpeg": "video/mpeg",
".mpg": "video/mpeg",
".ogg": "audio/ogg",
".ogv": "video/ogg",
".otf": "font/otf",
".pdf": "application/pdf",
".png": "image/png",
".qt": "video/quicktime",
".rar": "application/rar",
".rtf": "application/rtf",
".svg": "image/svg+xml",
".tar": "application/x-tar",
".tiff": "image/tiff",
".ts": "video/MP2T",
".ttc": "font/collection",
".ttf": "font/ttf",
".txt": "text/plain; charset=utf-8",
".wasm": "application/wasm",
".wav": "audio/x-wav",
".webm": "video/webm",
".webp": "image/webp",
".woff": "font/woff",
".woff2": "font/woff2",
".xml": "text/xml; charset=utf-8",
".xz": "application/x-xz",
".zip": "application/zip",
}
)
func mimeTypeByExtension(ext string) string {
mimeType, ok := mimeTypes[ext]
if ok {
return mimeType
}
return defaultMimeType
}