diff --git a/firewall/dialer.go b/firewall/dialer.go deleted file mode 100644 index 270b3f9f..00000000 --- a/firewall/dialer.go +++ /dev/null @@ -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 -} diff --git a/firewall/interception.go b/firewall/interception.go index 025a6c55..4edc790c 100644 --- a/firewall/interception.go +++ b/firewall/interception.go @@ -3,12 +3,14 @@ package firewall import ( "context" "errors" + "fmt" "net" "os" "sync/atomic" "time" "github.com/safing/portmaster/netenv" + "golang.org/x/sync/singleflight" "github.com/tevino/abool" @@ -63,7 +65,6 @@ func interceptionStart() error { interceptionModule.StartWorker("stat logger", statLogger) interceptionModule.StartWorker("packet handler", packetHandler) - interceptionModule.StartWorker("ports state cleaner", portsInUseCleaner) return interception.Start() } @@ -103,20 +104,60 @@ func handlePacket(ctx context.Context, pkt packet.Packet) { } pkt.SetCtx(traceCtx) - // associate packet to link and handle - conn, ok := network.GetConnection(pkt.GetConnectionID()) - if ok { - tracer.Tracef("filter: assigned to connection %s", conn.ID) - } else { - conn = network.NewConnectionFromFirstPacket(pkt) - tracer.Tracef("filter: created new connection %s", conn.ID) - conn.SetFirewallHandler(initialHandler) + // Get connection of packet. + conn, err := getConnection(pkt) + if err != nil { + tracer.Errorf("filter: packet %s dropped: %s", pkt, err) + _ = pkt.Drop() + return } // handle packet 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. func fastTrackedPermit(pkt packet.Packet) (handled bool) { meta := pkt.Info() @@ -265,13 +306,12 @@ func fastTrackedPermit(pkt packet.Packet) (handled bool) { func initialHandler(conn *network.Connection, pkt packet.Packet) { log.Tracer(pkt.Ctx()).Trace("filter: handing over to connection-based handler") - // check for internal firewall bypass - ps := getPortStatusAndMarkUsed(pkt.Info().LocalPort()) - if ps.isMe { - // approve + // Check for pre-authenticated port. + if localPortIsPreAuthenticated(conn.Entity.Protocol, conn.LocalPort) { + // Approve connection. conn.Accept("connection by Portmaster", noReasonOptionKey) conn.Internal = true - // finish + // Finalize connection. conn.StopFirewallHandler() issueVerdict(conn, pkt, 0, true) return diff --git a/firewall/ports.go b/firewall/ports.go deleted file mode 100644 index d8a61660..00000000 --- a/firewall/ports.go +++ /dev/null @@ -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) - } - } -} diff --git a/firewall/preauth.go b/firewall/preauth.go new file mode 100644 index 00000000..a2ab7322 --- /dev/null +++ b/firewall/preauth.go @@ -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)) +} diff --git a/network/ports.go b/network/ports.go new file mode 100644 index 00000000..0a4fa43d --- /dev/null +++ b/network/ports.go @@ -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 +} diff --git a/ui/serve.go b/ui/serve.go index 1e054c81..b7d0d401 100644 --- a/ui/serve.go +++ b/ui/serve.go @@ -3,7 +3,6 @@ package ui import ( "fmt" "io" - "mime" "net/http" "net/url" "path/filepath" @@ -143,7 +142,7 @@ func ServeFileFromBundle(w http.ResponseWriter, r *http.Request, bundleName stri // set content type _, ok := w.Header()["Content-Type"] if !ok { - contentType := mime.TypeByExtension(filepath.Ext(path)) + contentType := mimeTypeByExtension(filepath.Ext(path)) if 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) { 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 +}