github.com/meulengracht/snapd@v0.0.0-20210719210640-8bde69bcc84e/overlord/hookstate/ctlcmd/is_connected.go (about)

     1  // -*- Mode: Go; indent-tabs-mode: t -*-
     2  
     3  /*
     4   * Copyright (C) 2019 Canonical Ltd
     5   *
     6   * This program is free software: you can redistribute it and/or modify
     7   * it under the terms of the GNU General Public License version 3 as
     8   * published by the Free Software Foundation.
     9   *
    10   * This program is distributed in the hope that it will be useful,
    11   * but WITHOUT ANY WARRANTY; without even the implied warranty of
    12   * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    13   * GNU General Public License for more details.
    14   *
    15   * You should have received a copy of the GNU General Public License
    16   * along with this program.  If not, see <http://www.gnu.org/licenses/>.
    17   *
    18   */
    19  
    20  package ctlcmd
    21  
    22  import (
    23  	"fmt"
    24  
    25  	"github.com/snapcore/snapd/i18n"
    26  	"github.com/snapcore/snapd/interfaces"
    27  	"github.com/snapcore/snapd/overlord/ifacestate"
    28  	"github.com/snapcore/snapd/overlord/snapstate"
    29  	"github.com/snapcore/snapd/sandbox/apparmor"
    30  	"github.com/snapcore/snapd/sandbox/cgroup"
    31  	"github.com/snapcore/snapd/snap"
    32  )
    33  
    34  var cgroupSnapNameFromPid = cgroup.SnapNameFromPid
    35  
    36  const (
    37  	classicSnapCode = 10
    38  	notASnapCode    = 11
    39  )
    40  
    41  type isConnectedCommand struct {
    42  	baseCommand
    43  
    44  	Positional struct {
    45  		PlugOrSlotSpec string `positional-args:"true" positional-arg-name:"<plug|slot>"`
    46  	} `positional-args:"true" required:"true"`
    47  	Pid           int    `long:"pid" description:"Process ID for a plausibly connected process"`
    48  	AppArmorLabel string `long:"apparmor-label" description:"AppArmor label for a plausibly connected process"`
    49  }
    50  
    51  var shortIsConnectedHelp = i18n.G(`Return success if the given plug or slot is connected, and failure otherwise`)
    52  var longIsConnectedHelp = i18n.G(`
    53  The is-connected command returns success if the given plug or slot of the
    54  calling snap is connected, and failure otherwise.
    55  
    56  $ snapctl is-connected plug
    57  $ echo $?
    58  1
    59  
    60  Snaps can only query their own plugs and slots - snap name is implicit and
    61  implied by the snapctl execution context.
    62  
    63  The --pid and --aparmor-label options can be used to determine whether
    64  a plug or slot is connected to the snap identified by the given
    65  process ID or AppArmor label.  In this mode, additional failure exit
    66  codes may be returned: 10 if the other snap is not connected but uses
    67  classic confinement, or 11 if the other process is not snap confined.
    68  
    69  The --pid and --apparmor-label options may only be used with slots of
    70  interface type "pulseaudio", "audio-record", or "cups-control".
    71  `)
    72  
    73  func init() {
    74  	addCommand("is-connected", shortIsConnectedHelp, longIsConnectedHelp, func() command {
    75  		return &isConnectedCommand{}
    76  	})
    77  }
    78  
    79  func isConnectedPidCheckAllowed(info *snap.Info, plugOrSlot string) bool {
    80  	slot := info.Slots[plugOrSlot]
    81  	if slot != nil {
    82  		switch slot.Interface {
    83  		case "pulseaudio", "audio-record", "cups-control":
    84  			return true
    85  		}
    86  	}
    87  	return false
    88  }
    89  
    90  func (c *isConnectedCommand) Execute(args []string) error {
    91  	plugOrSlot := c.Positional.PlugOrSlotSpec
    92  
    93  	context := c.context()
    94  	if context == nil {
    95  		return fmt.Errorf("cannot check connection status without a context")
    96  	}
    97  
    98  	snapName := context.InstanceName()
    99  
   100  	st := context.State()
   101  	st.Lock()
   102  	defer st.Unlock()
   103  
   104  	info, err := snapstate.CurrentInfo(st, snapName)
   105  	if err != nil {
   106  		return fmt.Errorf("internal error: cannot get snap info: %s", err)
   107  	}
   108  
   109  	// XXX: This will fail for implicit slots.  In practice, this
   110  	// would only affect calls that used the "core" snap as
   111  	// context.  That snap does not have any hooks using
   112  	// is-connected, so the limitation is probably moot.
   113  	if info.Plugs[plugOrSlot] == nil && info.Slots[plugOrSlot] == nil {
   114  		return fmt.Errorf("snap %q has no plug or slot named %q", snapName, plugOrSlot)
   115  	}
   116  
   117  	conns, err := ifacestate.ConnectionStates(st)
   118  	if err != nil {
   119  		return fmt.Errorf("internal error: cannot get connections: %s", err)
   120  	}
   121  
   122  	var otherSnap *snap.Info
   123  	if c.AppArmorLabel != "" {
   124  		if !isConnectedPidCheckAllowed(info, plugOrSlot) {
   125  			return fmt.Errorf("cannot use --apparmor-label check with %s:%s", snapName, plugOrSlot)
   126  		}
   127  		name, _, _, err := apparmor.DecodeLabel(c.AppArmorLabel)
   128  		if err != nil {
   129  			return &UnsuccessfulError{ExitCode: notASnapCode}
   130  		}
   131  		otherSnap, err = snapstate.CurrentInfo(st, name)
   132  		if err != nil {
   133  			return fmt.Errorf("internal error: cannot get snap info for AppArmor label %q: %s", c.AppArmorLabel, err)
   134  		}
   135  	} else if c.Pid != 0 {
   136  		if !isConnectedPidCheckAllowed(info, plugOrSlot) {
   137  			return fmt.Errorf("cannot use --pid check with %s:%s", snapName, plugOrSlot)
   138  		}
   139  		name, err := cgroupSnapNameFromPid(c.Pid)
   140  		if err != nil {
   141  			// Indicate that this pid is not a snap
   142  			return &UnsuccessfulError{ExitCode: notASnapCode}
   143  		}
   144  		otherSnap, err = snapstate.CurrentInfo(st, name)
   145  		if err != nil {
   146  			return fmt.Errorf("internal error: cannot get snap info for pid %d: %s", c.Pid, err)
   147  		}
   148  	}
   149  
   150  	// snapName is the name of the snap executing snapctl command, it's
   151  	// obtained from the context (ephemeral if run by apps, or full if run by
   152  	// hooks). plug and slot names are unique within a snap, so there is no
   153  	// ambiguity when matching.
   154  	for refStr, connState := range conns {
   155  		if connState.Undesired || connState.HotplugGone {
   156  			continue
   157  		}
   158  		connRef, err := interfaces.ParseConnRef(refStr)
   159  		if err != nil {
   160  			return fmt.Errorf("internal error: %s", err)
   161  		}
   162  
   163  		matchingPlug := connRef.PlugRef.Snap == snapName && connRef.PlugRef.Name == plugOrSlot
   164  		matchingSlot := connRef.SlotRef.Snap == snapName && connRef.SlotRef.Name == plugOrSlot
   165  		if otherSnap != nil {
   166  			if matchingPlug && connRef.SlotRef.Snap == otherSnap.InstanceName() || matchingSlot && connRef.PlugRef.Snap == otherSnap.InstanceName() {
   167  				return nil
   168  			}
   169  		} else {
   170  			if matchingPlug || matchingSlot {
   171  				return nil
   172  			}
   173  		}
   174  	}
   175  
   176  	if otherSnap != nil && otherSnap.Confinement == snap.ClassicConfinement {
   177  		return &UnsuccessfulError{ExitCode: classicSnapCode}
   178  	}
   179  
   180  	return &UnsuccessfulError{ExitCode: 1}
   181  }