platform_windows.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281
  1. //go:build windows
  2. package localapi
  3. import (
  4. "errors"
  5. "fmt"
  6. "log/slog"
  7. "net"
  8. "strings"
  9. "time"
  10. "github.com/Microsoft/go-winio"
  11. "golang.org/x/sys/windows"
  12. )
  13. // pipePrefix is where windows named pipes live. The pipe's own security descriptor
  14. // carries the access control that the socket's mode carries on unix.
  15. const pipePrefix = `\\.\pipe\`
  16. // protectedPrefix is the pipe namespace that only administrators and SYSTEM may create
  17. // names under; the kernel refuses everyone else. The default name lives there, so the
  18. // name itself says who could have created the pipe answering it: no ordinary user can
  19. // have put anything under it ahead of the daemon. Tailscale, WireGuard and NetBird keep
  20. // their daemon pipes there for the same reason. nebula on windows is already an
  21. // administrator, since creating the tun adapter requires it.
  22. const protectedPrefix = pipePrefix + `ProtectedPrefix\Administrators\`
  23. const network = "pipe"
  24. // currentUID is the identity the unix owner checks are made for. There is no uid here
  25. // and no per-connection check that would read one, so the value is never consulted; it
  26. // exists so a Server is built the same way on every platform.
  27. var currentUID = func() uint32 { return 0 }
  28. // logListening is the line written when the pipe is up. It says which layer is doing
  29. // the access control, so nobody assumes a per-connection check that is not running:
  30. // there is none here, the pipe's security descriptor admits only the accounts listed in
  31. // ownerOnlySDDL, and the kernel enforces that at connect time.
  32. func logListening(l *slog.Logger, path string, acc Access) {
  33. l.Info("Local control socket listening, access restricted by the pipe's security descriptor",
  34. "path", path,
  35. "access", acc.String(),
  36. "reason", "the security descriptor built at listen time is the access check; the connecting process is not identified separately",
  37. )
  38. }
  39. // listen creates the named pipe at path with a security descriptor that admits the user
  40. // nebula runs as, SYSTEM and Administrators, and the group acc names, if any, for
  41. // reading and writing, which is what connecting to a pipe takes. acc's mode means
  42. // nothing to a pipe.
  43. //
  44. // The first instance is created, never opened, so a name some other process already
  45. // holds is refused rather than joined: that process would otherwise answer clients in
  46. // nebula's place. Under protectedPrefix only an administrator could hold it; anywhere
  47. // else any account could. Either way the refusal is reported as errEndpointHeld for the
  48. // caller to log as the event it is, except when the refusal is the namespace's own: a
  49. // daemon that is not an administrator cannot create under protectedPrefix at all, and
  50. // that is told apart so the operator is sent to elevate or to name a pipe elsewhere
  51. // rather than to look for a process that is not there.
  52. func listen(path string, acc Access) (net.Listener, error) {
  53. sddl, err := sddlFor(acc)
  54. if err != nil {
  55. return nil, fmt.Errorf("could not build a security descriptor for %s: %w", path, err)
  56. }
  57. ln, err := winio.ListenPipe(path, &winio.PipeConfig{
  58. SecurityDescriptor: sddl,
  59. InputBufferSize: int32(MaxRequestBytes),
  60. OutputBufferSize: 64 * 1024,
  61. })
  62. if err != nil {
  63. return nil, listenRefused(path, isAdministrator(), err)
  64. }
  65. return ln, nil
  66. }
  67. // listenRefused turns a failure to create the pipe at path into the error the caller
  68. // logs. admin says whether this process may create under protectedPrefix.
  69. //
  70. // ERROR_ACCESS_DENIED is ambiguous: it is what the kernel says to a non-administrator
  71. // creating under protectedPrefix, and also what an existing instance whose DACL does
  72. // not admit us says to a second create. A process that is an administrator cannot be
  73. // getting the first, so for it, and for every name outside the prefix, access denied
  74. // means held.
  75. func listenRefused(path string, admin bool, err error) error {
  76. if errors.Is(err, windows.ERROR_ACCESS_DENIED) && isProtectedPath(path) && !admin {
  77. return fmt.Errorf("creating %s requires an administrator, as only administrators may create pipes under %s; run nebula elevated, or set %s to a pipe name outside it: %w", path, protectedPrefix, CtlSocketKey, err)
  78. }
  79. if errors.Is(err, windows.ERROR_ALREADY_EXISTS) || errors.Is(err, windows.ERROR_ACCESS_DENIED) {
  80. return fmt.Errorf("%w: %s: %w", errEndpointHeld, path, err)
  81. }
  82. return err
  83. }
  84. // isProtectedPath says whether path is a name under protectedPrefix. Pipe names are
  85. // case insensitive, so the comparison is too.
  86. func isProtectedPath(path string) bool {
  87. return len(path) > len(protectedPrefix) && strings.EqualFold(path[:len(protectedPrefix)], protectedPrefix)
  88. }
  89. // isAdministrator says whether this process holds Administrators as an enabled group,
  90. // which is what creating under protectedPrefix takes. SYSTEM holds it; an elevated
  91. // administrator holds it; an administrator's unelevated token carries it deny-only,
  92. // which the membership check correctly reads as no. The membership is checked against
  93. // the thread's own token: CheckTokenMembership wants an impersonation token and makes
  94. // one from the primary token when handed none. A check that cannot be made is read as
  95. // not an administrator, which only changes the wording of a failure that has already
  96. // happened.
  97. func isAdministrator() bool {
  98. admins, err := windows.CreateWellKnownSid(windows.WinBuiltinAdministratorsSid)
  99. if err != nil {
  100. return false
  101. }
  102. member, err := windows.Token(0).IsMember(admins)
  103. return err == nil && member
  104. }
  105. // sddlFor grants full access to the account nebula is running as, to SYSTEM and to
  106. // Administrators, and to the group acc names, if any, what a client's connect takes
  107. // and no more; nobody else. P makes the DACL protected, so it does not inherit entries
  108. // that would widen it. It is dockerd's descriptor for its pipe, with its -G group in the
  109. // same place, except for the group's mask: dockerd grants GENERIC_READ|GENERIC_WRITE,
  110. // and on a pipe GENERIC_WRITE carries FILE_CREATE_PIPE_INSTANCE, which would let a
  111. // member create further instances under the name and answer other clients itself. The
  112. // mask here is FILE_GENERIC_READ|FILE_GENERIC_WRITE with that right left out, which
  113. // opens the pipe and reads and writes it and nothing else.
  114. //
  115. // Administrators is the counterpart of the uid 0 allowance in the unix peer check. The
  116. // service normally runs as LocalSystem, so without it an operator in an elevated shell
  117. // could not reach the daemon at all; and an administrator can already stop the service,
  118. // so this grants no authority they did not have.
  119. func sddlFor(acc Access) (string, error) {
  120. token := windows.GetCurrentProcessToken()
  121. user, err := token.GetTokenUser()
  122. if err != nil {
  123. return "", err
  124. }
  125. sddl := fmt.Sprintf("D:P(A;;GA;;;%s)(A;;GA;;;SY)(A;;GA;;;BA)", user.User.Sid.String())
  126. if acc.Group != nil {
  127. sddl += fmt.Sprintf("(A;;%s;;;%s)", groupPipeMask, acc.Group.ID)
  128. }
  129. return sddl, nil
  130. }
  131. // groupPipeMask is the access a configured group gets on the pipe: FILE_GENERIC_READ
  132. // (0x120089) and FILE_GENERIC_WRITE (0x120116) without FILE_CREATE_PIPE_INSTANCE (0x4),
  133. // written as SDDL writes a mask it has no letters for.
  134. const groupPipeMask = "0x12019b"
  135. // removeSocket is a no-op: a named pipe disappears when its last handle closes, so
  136. // there is nothing to unlink and no stale instance to clean up.
  137. func removeSocket(path string, id socketID) {}
  138. // identifySocket has no node to identify: a pipe is its handle, and it cannot be
  139. // removed from under the daemon, so an endpoint on its path is always still there.
  140. func identifySocket(path string) (socketID, bool) { return socketID{}, true }
  141. // applyAccess cannot change a pipe's access in place: its security descriptor is fixed
  142. // when it is created, so a change means creating it again, which the caller does.
  143. func applyAccess(path string, acc Access) error { return errReopenForAccess }
  144. // dial connects to the pipe and, before the connection carries anything, checks who
  145. // serves it. The request names a command and its arguments, so it must not reach a
  146. // pipe that some other account put under nebula's name. For the default name the
  147. // namespace already promises that, since only an administrator could have created it;
  148. // the check is what covers a CtlSocketKey override outside protectedPrefix, and costs
  149. // nothing to make on every name.
  150. func dial(path string, timeout time.Duration) (net.Conn, error) {
  151. conn, err := winio.DialPipe(path, &timeout)
  152. if err != nil {
  153. return nil, err
  154. }
  155. if err := checkPipeOwner(conn, path); err != nil {
  156. conn.Close()
  157. return nil, err
  158. }
  159. return conn, nil
  160. }
  161. // checkPipeOwner refuses a pipe whose owner is not an account that could be running
  162. // nebula. It is the unix owner check made from the other side: the handle winio dialed
  163. // exposes the pipe object, and the object's owner is whoever created it.
  164. //
  165. // The owner is what makes this sound. An account can only create an object owned by
  166. // itself, or by a SID it holds with the owner flag, unless it has SeRestorePrivilege,
  167. // which ordinary users do not; so a pipe owned by SYSTEM, Administrators or a service
  168. // account was put there by one of them, and never by a user squatting the name. winio
  169. // dials with SECURITY_ANONYMOUS, so the impostor could not have impersonated the client
  170. // either, but it could have read the command and answered it with whatever it liked.
  171. func checkPipeOwner(conn net.Conn, path string) error {
  172. h, ok := conn.(interface{ Fd() uintptr })
  173. if !ok {
  174. return fmt.Errorf("%s: cannot identify who serves the pipe", path)
  175. }
  176. sd, err := windows.GetSecurityInfo(windows.Handle(h.Fd()), windows.SE_KERNEL_OBJECT, windows.OWNER_SECURITY_INFORMATION)
  177. if err != nil {
  178. return fmt.Errorf("%s: could not read the pipe's owner: %w", path, err)
  179. }
  180. owner, _, err := sd.Owner()
  181. if err != nil {
  182. return fmt.Errorf("%s: could not read the pipe's owner: %w", path, err)
  183. }
  184. if owner == nil {
  185. return fmt.Errorf("%s has no owner; refusing to send to it", path)
  186. }
  187. me, err := windows.GetCurrentProcessToken().GetTokenUser()
  188. if err != nil {
  189. return fmt.Errorf("%s: could not read own identity: %w", path, err)
  190. }
  191. if ownerAccepted(owner, me.User.Sid) {
  192. return nil
  193. }
  194. return fmt.Errorf("%s is served by %s, not by nebula; run nebula ctl as that user, or pass -config or -socket to name the daemon's pipe", path, describeSID(owner))
  195. }
  196. // ownerAccepted says whether a pipe owned by owner may be sent a request by a client
  197. // running as me. Accepted are the client's own account and the accounts a daemon runs
  198. // under that no ordinary user can create an object as: SYSTEM, Administrators, the
  199. // LocalService and NetworkService accounts, and the per-service virtual accounts under
  200. // NT SERVICE (S-1-5-80-...). Another user's account is refused, whoever the client is;
  201. // the daemon's DACL decides who may connect, and this decides who may answer.
  202. func ownerAccepted(owner, me *windows.SID) bool {
  203. if owner.Equals(me) {
  204. return true
  205. }
  206. if owner.IsWellKnown(windows.WinLocalSystemSid) ||
  207. owner.IsWellKnown(windows.WinBuiltinAdministratorsSid) ||
  208. owner.IsWellKnown(windows.WinLocalServiceSid) ||
  209. owner.IsWellKnown(windows.WinNetworkServiceSid) {
  210. return true
  211. }
  212. // NT SERVICE\<name>: S-1-5-80 followed by the five sub-authorities of the service's
  213. // name hash. The well-known SID table has no entry for them, as each is distinct.
  214. const serviceBaseRID = 80
  215. return owner.IdentifierAuthority() == windows.SECURITY_NT_AUTHORITY &&
  216. owner.SubAuthorityCount() == 6 &&
  217. owner.SubAuthority(0) == serviceBaseRID
  218. }
  219. // describeSID renders a SID with its account name when the host can say what it is, so
  220. // a message about somebody else's pipe tells the operator who that is.
  221. func describeSID(sid *windows.SID) string {
  222. account, domain, _, err := sid.LookupAccount("")
  223. if err != nil || account == "" {
  224. return sid.String()
  225. }
  226. if domain == "" {
  227. return account
  228. }
  229. return domain + `\` + account
  230. }
  231. // defaultSocketPath is the control pipe when the config names none. It is under
  232. // protectedPrefix, so whatever answers it was created by an administrator or SYSTEM; see
  233. // there.
  234. const defaultSocketPath = protectedPrefix + "nebula-ctl"
  235. // checkSocketPath refuses a configured name that is not a full pipe name. A pipe lives
  236. // under pipePrefix and nowhere else, so a bare name has no meaning here, and one under
  237. // pipePrefix is used as given. An override outside protectedPrefix is a name any
  238. // account could have created first; the owner check in dial is what protects a client
  239. // of one.
  240. func checkSocketPath(key, value string) error {
  241. if len(value) <= len(pipePrefix) || !strings.EqualFold(value[:len(pipePrefix)], pipePrefix) {
  242. return fmt.Errorf(`%s must be a full pipe name under %s, got %q`, key, pipePrefix, value)
  243. }
  244. return nil
  245. }