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 }