platform_windows.go 12 KB

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