safing-portmaster/service/firewall/api.go

226 lines
6.6 KiB
Go

package firewall
import (
"context"
"fmt"
"net"
"net/http"
"path/filepath"
"slices"
"strings"
"time"
"github.com/safing/portbase/api"
"github.com/safing/portbase/dataroot"
"github.com/safing/portbase/log"
"github.com/safing/portbase/utils"
"github.com/safing/portmaster/service/netenv"
"github.com/safing/portmaster/service/network/netutils"
"github.com/safing/portmaster/service/network/packet"
"github.com/safing/portmaster/service/process"
"github.com/safing/portmaster/service/updates"
)
const (
deniedMsgUnidentified = `%wFailed to identify the requesting process. Reload to try again.`
deniedMsgSystem = `%wSystem access to the Portmaster API is not permitted.
You can enable the Development Mode to disable API authentication for development purposes.`
deniedMsgUnauthorized = `%wThe requesting process is not authorized to access the Portmaster API.
Checked process paths:
%s
The authorized root path is %s.
You can enable the Development Mode to disable API authentication for development purposes.
For production use please create an API key in the settings.`
deniedMsgMisconfigured = `%wThe authentication system is misconfigured.`
)
var (
dataRoot *utils.DirStructure
apiPortSet bool
apiIP net.IP
apiPort uint16
)
func prepAPIAuth() error {
dataRoot = dataroot.Root()
return api.SetAuthenticator(apiAuthenticator)
}
func startAPIAuth() {
var err error
apiIP, apiPort, err = netutils.ParseIPPort(apiListenAddress())
if err != nil {
log.Warningf("filter: failed to parse API address for improved api auth mechanism: %s", err)
return
}
apiPortSet = true
log.Tracef("filter: api port set to %d", apiPort)
}
func apiAuthenticator(r *http.Request, s *http.Server) (token *api.AuthToken, err error) {
if configReady.IsSet() && devMode() {
return &api.AuthToken{
Read: api.PermitSelf,
Write: api.PermitSelf,
}, nil
}
// get local IP/Port
localIP, localPort, err := netutils.ParseIPPort(s.Addr)
if err != nil {
return nil, fmt.Errorf("failed to get local IP/Port: %w", err)
}
// Correct 0.0.0.0 to 127.0.0.1 to fix local process-based authentication,
// if 0.0.0.0 is used as the API listen address.
if localIP.Equal(net.IPv4zero) {
localIP = net.IPv4(127, 0, 0, 1)
}
// get remote IP/Port
remoteIP, remotePort, err := netutils.ParseIPPort(r.RemoteAddr)
if err != nil {
return nil, fmt.Errorf("failed to get remote IP/Port: %w", err)
}
// Check if the request is even local.
myIP, err := netenv.IsMyIP(remoteIP)
if err == nil && !myIP {
// Return to caller that the request was not handled.
return nil, nil
}
log.Tracer(r.Context()).Tracef("filter: authenticating API request from %s", r.RemoteAddr)
// It is important that this works, retry 5 times: every 500ms for 2.5s.
var retry bool
for tries := 0; tries < 5; tries++ {
retry, err = authenticateAPIRequest(
r.Context(),
&packet.Info{
Inbound: false, // outbound as we are looking for the process of the source address
Version: packet.IPv4,
Protocol: packet.TCP,
Src: remoteIP, // source as in the process we are looking for
SrcPort: remotePort, // source as in the process we are looking for
Dst: localIP,
DstPort: localPort,
PID: process.UndefinedProcessID,
},
)
if !retry {
break
}
// wait a little
time.Sleep(500 * time.Millisecond)
}
if err != nil {
return nil, err
}
return &api.AuthToken{
Read: api.PermitSelf,
Write: api.PermitSelf,
}, nil
}
func authenticateAPIRequest(ctx context.Context, pktInfo *packet.Info) (retry bool, err error) {
var procsChecked []string
var originalPid int
// Get authenticated path.
authenticatedPath := updates.RootPath()
if authenticatedPath == "" {
return false, fmt.Errorf(deniedMsgMisconfigured, api.ErrAPIAccessDeniedMessage) //nolint:stylecheck // message for user
}
// Get real path.
authenticatedPath, err = filepath.EvalSymlinks(authenticatedPath)
if err != nil {
return false, fmt.Errorf(deniedMsgUnidentified, api.ErrAPIAccessDeniedMessage) //nolint:stylecheck // message for user
}
// Add filepath separator to confine to directory.
authenticatedPath += string(filepath.Separator)
// Get process of request.
pid, _, _ := process.GetPidOfConnection(ctx, pktInfo)
if pid < 0 {
return false, fmt.Errorf(deniedMsgUnidentified, api.ErrAPIAccessDeniedMessage) //nolint:stylecheck // message for user
}
proc, err := process.GetOrFindProcess(ctx, pid)
if err != nil {
log.Tracer(ctx).Debugf("filter: failed to get process of api request: %s", err)
originalPid = process.UnidentifiedProcessID
} else {
originalPid = proc.Pid
var previousPid int
// Find parent for up to two levels, if we don't match the path.
checkLevels := 2
checkLevelsLoop:
for i := 0; i < checkLevels+1; i++ {
// Check for eligible path.
switch proc.Pid {
case process.UnidentifiedProcessID, process.SystemProcessID:
break checkLevelsLoop
default: // normal process
// Check if the requesting process is in database root / updates dir.
if realPath, err := filepath.EvalSymlinks(proc.Path); err == nil {
// check if the client has been allowed by flag
if slices.Contains(allowedClients, realPath) {
return false, nil
}
if strings.HasPrefix(realPath, authenticatedPath) {
return false, nil
}
}
}
// Add checked path to list.
procsChecked = append(procsChecked, proc.Path)
// Get the parent process.
if i < checkLevels {
// save previous PID
previousPid = proc.Pid
// get parent process
proc, err = process.GetOrFindProcess(ctx, proc.ParentPid)
if err != nil {
log.Tracer(ctx).Debugf("filter: failed to get parent process of api request: %s", err)
break
}
// abort if we are looping
if proc.Pid == previousPid {
// this also catches -1 pid loops
break
}
}
}
}
switch originalPid {
case process.UnidentifiedProcessID:
log.Tracer(ctx).Warningf("filter: denying api access: failed to identify process")
return true, fmt.Errorf(deniedMsgUnidentified, api.ErrAPIAccessDeniedMessage) //nolint:stylecheck // message for user
case process.SystemProcessID:
log.Tracer(ctx).Warningf("filter: denying api access: request by system")
return false, fmt.Errorf(deniedMsgSystem, api.ErrAPIAccessDeniedMessage) //nolint:stylecheck // message for user
default: // normal process
log.Tracer(ctx).Warningf("filter: denying api access to %s - also checked %s (trusted root is %s)", procsChecked[0], strings.Join(procsChecked[1:], " "), dataRoot.Path)
return false, fmt.Errorf( //nolint:stylecheck // message for user
deniedMsgUnauthorized,
api.ErrAPIAccessDeniedMessage,
strings.Join(procsChecked, "\n"),
authenticatedPath,
)
}
}