platform_windows.go 12 KB

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