platform_windows.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272
  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 read and write, which is what a client's connect takes, to the
  107. // group acc names, if any; nobody else. P makes the DACL protected, so it does not
  108. // inherit entries that would widen it. It is dockerd's descriptor for its pipe, with
  109. // its -G group in the same place.
  110. //
  111. // Administrators is the counterpart of the uid 0 allowance in the unix peer check. The
  112. // service normally runs as LocalSystem, so without it an operator in an elevated shell
  113. // could not reach the daemon at all; and an administrator can already stop the service,
  114. // so this grants no authority they did not have.
  115. func sddlFor(acc Access) (string, error) {
  116. token := windows.GetCurrentProcessToken()
  117. user, err := token.GetTokenUser()
  118. if err != nil {
  119. return "", err
  120. }
  121. sddl := fmt.Sprintf("D:P(A;;GA;;;%s)(A;;GA;;;SY)(A;;GA;;;BA)", user.User.Sid.String())
  122. if acc.Group != nil {
  123. sddl += fmt.Sprintf("(A;;GRGW;;;%s)", acc.Group.ID)
  124. }
  125. return sddl, nil
  126. }
  127. // removeSocket is a no-op: a named pipe disappears when its last handle closes, so
  128. // there is nothing to unlink and no stale instance to clean up.
  129. func removeSocket(path string, id socketID) {}
  130. // identifySocket has no node to identify: a pipe is its handle, and it cannot be
  131. // removed from under the daemon, so an endpoint on its path is always still there.
  132. func identifySocket(path string) (socketID, bool) { return socketID{}, true }
  133. // applyAccess cannot change a pipe's access in place: its security descriptor is fixed
  134. // when it is created, so a change means creating it again, which the caller does.
  135. func applyAccess(path string, acc Access) error { return errReopenForAccess }
  136. // dial connects to the pipe and, before the connection carries anything, checks who
  137. // serves it. The request names a command and its arguments, so it must not reach a
  138. // pipe that some other account put under nebula's name. For the default name the
  139. // namespace already promises that, since only an administrator could have created it;
  140. // the check is what covers a CtlSocketKey override outside protectedPrefix, and costs
  141. // nothing to make on every name.
  142. func dial(path string, timeout time.Duration) (net.Conn, error) {
  143. conn, err := winio.DialPipe(path, &timeout)
  144. if err != nil {
  145. return nil, err
  146. }
  147. if err := checkPipeOwner(conn, path); err != nil {
  148. conn.Close()
  149. return nil, err
  150. }
  151. return conn, nil
  152. }
  153. // checkPipeOwner refuses a pipe whose owner is not an account that could be running
  154. // nebula. It is the unix owner check made from the other side: the handle winio dialed
  155. // exposes the pipe object, and the object's owner is whoever created it.
  156. //
  157. // The owner is what makes this sound. An account can only create an object owned by
  158. // itself, or by a SID it holds with the owner flag, unless it has SeRestorePrivilege,
  159. // which ordinary users do not; so a pipe owned by SYSTEM, Administrators or a service
  160. // account was put there by one of them, and never by a user squatting the name. winio
  161. // dials with SECURITY_ANONYMOUS, so the impostor could not have impersonated the client
  162. // either, but it could have read the command and answered it with whatever it liked.
  163. func checkPipeOwner(conn net.Conn, path string) error {
  164. h, ok := conn.(interface{ Fd() uintptr })
  165. if !ok {
  166. return fmt.Errorf("%s: cannot identify who serves the pipe", path)
  167. }
  168. sd, err := windows.GetSecurityInfo(windows.Handle(h.Fd()), windows.SE_KERNEL_OBJECT, windows.OWNER_SECURITY_INFORMATION)
  169. if err != nil {
  170. return fmt.Errorf("%s: could not read the pipe's owner: %w", path, err)
  171. }
  172. owner, _, err := sd.Owner()
  173. if err != nil {
  174. return fmt.Errorf("%s: could not read the pipe's owner: %w", path, err)
  175. }
  176. if owner == nil {
  177. return fmt.Errorf("%s has no owner; refusing to send to it", path)
  178. }
  179. me, err := windows.GetCurrentProcessToken().GetTokenUser()
  180. if err != nil {
  181. return fmt.Errorf("%s: could not read own identity: %w", path, err)
  182. }
  183. if ownerAccepted(owner, me.User.Sid) {
  184. return nil
  185. }
  186. 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))
  187. }
  188. // ownerAccepted says whether a pipe owned by owner may be sent a request by a client
  189. // running as me. Accepted are the client's own account and the accounts a daemon runs
  190. // under that no ordinary user can create an object as: SYSTEM, Administrators, the
  191. // LocalService and NetworkService accounts, and the per-service virtual accounts under
  192. // NT SERVICE (S-1-5-80-...). Another user's account is refused, whoever the client is;
  193. // the daemon's DACL decides who may connect, and this decides who may answer.
  194. func ownerAccepted(owner, me *windows.SID) bool {
  195. if owner.Equals(me) {
  196. return true
  197. }
  198. if owner.IsWellKnown(windows.WinLocalSystemSid) ||
  199. owner.IsWellKnown(windows.WinBuiltinAdministratorsSid) ||
  200. owner.IsWellKnown(windows.WinLocalServiceSid) ||
  201. owner.IsWellKnown(windows.WinNetworkServiceSid) {
  202. return true
  203. }
  204. // NT SERVICE\<name>: S-1-5-80 followed by the five sub-authorities of the service's
  205. // name hash. The well-known SID table has no entry for them, as each is distinct.
  206. const serviceBaseRID = 80
  207. return owner.IdentifierAuthority() == windows.SECURITY_NT_AUTHORITY &&
  208. owner.SubAuthorityCount() == 6 &&
  209. owner.SubAuthority(0) == serviceBaseRID
  210. }
  211. // describeSID renders a SID with its account name when the host can say what it is, so
  212. // a message about somebody else's pipe tells the operator who that is.
  213. func describeSID(sid *windows.SID) string {
  214. account, domain, _, err := sid.LookupAccount("")
  215. if err != nil || account == "" {
  216. return sid.String()
  217. }
  218. if domain == "" {
  219. return account
  220. }
  221. return domain + `\` + account
  222. }
  223. // defaultSocketPath is the control pipe when the config names none. It is under
  224. // protectedPrefix, so whatever answers it was created by an administrator or SYSTEM; see
  225. // there.
  226. const defaultSocketPath = protectedPrefix + "nebula-ctl"
  227. // checkSocketPath refuses a configured name that is not a full pipe name. A pipe lives
  228. // under pipePrefix and nowhere else, so a bare name has no meaning here, and one under
  229. // pipePrefix is used as given. An override outside protectedPrefix is a name any
  230. // account could have created first; the owner check in dial is what protects a client
  231. // of one.
  232. func checkSocketPath(key, value string) error {
  233. if len(value) <= len(pipePrefix) || !strings.EqualFold(value[:len(pipePrefix)], pipePrefix) {
  234. return fmt.Errorf(`%s must be a full pipe name under %s, got %q`, key, pipePrefix, value)
  235. }
  236. return nil
  237. }