Named Pipes Emulation layer --------------------------- Author: lkcl@lkcl.net Comments should be directed at: http://wiki.winehq.org/NamedPipes Introduction ------------ For Wine, Samba, Samba TNG and other applications to interoperate, in particular but not limited to cooperation involving MSRPC services, the common point between them is an emulation of Windows NT Named Pipes, over which not only MSRPC is proxied using ncacn_np but also Wine applications will expect to be able to create their own remote NT Named Pipes. The situation at present, where samba has been "tying up" Named Pipes by treating all incoming Named Pipe traffic as "internal and proprietary to Samba" can be charitably described as "untenable", at best. Interoperability is essential for developers, users and for all projects to move forward, utilising the best components - MSRPC infrastructure and service - from each project. Wine's current NamedPipes implementation is incomplete, lacking MessageMode semantics. Perhaps not unsurprisingly, implementation of MessageMode NamedPipes semantics in Wine can in many ways be best done by implementing Samba and Samba TNG interoperability! Not only will testing be easier, but also the internal architecture of wineserver is startlingly similar to that of an SMB server - not surprising given that NT's internal design is based around MACH Kernel (message-passing, just like in wineserver), and that the design of SMB in NT was originally quite literally shoving its internal message-like "structs" out over-the-wire! So a simple and shared "SMB-like", "Wineserver-like" protocol, given the similarity between Wine and Samba / Samba TNG internals, makes a lot of sense. As such, the specification in this document is similar to that of the CIFS specification, but is intended for "internal" use between processes on the same machine (not for inter-machine remote communication: that job is already covered, in the form of SMB itself). What this document does NOT define ---------------------------------- * This document does not dictate what the API that utilises the named pipe emulation layer looks like. So, for example, an implementation in Wine of CreateNamedPipe may perform NT-style security checks to determine whether the application has the right to actually create the pipe, but such security checks are NOT part of this specification: it is assumed in all instances that all such appropriate checks will have been properly performed BEFORE access to an emulated named pipe is allowed. * This document does not dictate the level of security or the requirements for authentication, but instead provides a means for different client and servers to interoperate according to the transfer of security context information that is entirely opaque to the named pipes emulation layer. * This document does NOT cover and is not intended for, networked traffic: the specification is intended for communication between processes on the SAME machine. "Remote" Named Pipes emulation will be provided on TOP of this specification, by SMB-aware services, such as Samba and Samba TNG, acting as networking "Proxies". Such proxying will take place purely at the "client" level. In other words, for example, a Wine "Remote" Named Pipe client will utilise libsmbclient or other suitable SMB client library, using SMBopenX, SMBtrans2 etc. to talk to a remote server; at the remote end, which MAY happen to be a Samba or a Samba TNG server, the SMB server will make a connection, on behalf of the client, to a waiting application. At NO TIME will the "CreateNamedPipe" operation EVER be "proxied" as it is just not possible to *CREATE* a Named Pipe on a remote system, just in exactly the same way as it is not possible to *create* a TCP/IP socket on a remote system. The public functions -------------------- CreateNamedPipe (server-side) ConnectNamedPipe (server-side) CreateFile (client-side with "\PIPE\xxx") WaitNamedPipe (client-side) SetNamedPipeHandleState GetNamedPipeHandleState PeekNamedPipe TransactNamedPipe ImpersonateNamedPipeClient NtReadFile (only on a named pipe handle) NtWriteFile (only on a named pipe handle) CloseHandle (only on a named pipe handle) In Samba 3, these are partially implemented as: CreateNamedPipe : not applicable / internally implemented ConnectNamedPipe : named_pipe in smbd/ipc.c NtCreateFile : reply_open_pipe_and_X in smbd/pipes.c WaitNamedPipe : api_WNPHS in smbd/ipc.c PeekNamedPipe : unknown. SetNamedPipeHandleState : api_SNPHS in smbd/ipc.c GetNamedPipeHandleState : unknown TransactNamedPipe : api_dcerpc_cmd in smbd/ipc.c ImpersonateNamedPipeClient: not applicable / internally implemented. NtReadFile : reply_pipe_read_and_X in smbd/reply.c NtWriteFile : reply_pipe_write_and_X in smbd/reply.c CloseHandle : close_fake_file in smbd/fake_file.c GetNamedPipeHandleState is *likely* to be implemented as call_trans2qpipeinfo() in smbd/trans2.c however this will have to be determined by experimental observation. In Samba TNG, these are partially implemented as: CreateNamedPipe : not applicable / internally implemented ConnectNamedPipe : named_pipe in smbd/ipc.c NtCreateFile : reply_open_pipe_and_X in smbd/pipes.c WaitNamedPipe : api_WNPHS in smbd/ipc.c PeekNamedPipe : unknown. SetNamedPipeHandleState : api_SNPHS in smbd/ipc.c GetNamedPipeHandleState : unknown TransactNamedPipe : read_then_write_pipe in smbd/ipc.c ImpersonateNamedPipeClient: not applicable / internally implemented. NtReadFile : reply_pipe_read_and_X in smbd/reply.c NtWriteFile : reply_pipe_write_and_X in smbd/reply.c CloseHandle : reply_pipe_close in smbd/reply.c GetNamedPipeHandleState is *not* implemented in Samba TNG. Named Pipe functionality client-side ends up in lib/msrpc-client.c and server-side in smbd/pipe-proxy.c In Wine, these functions are partially implemented as: CreateNamedPipe : create_named_pipe in server/named_pipes.c ConnectNamedPipe : fully in dlls/kernel32/sync.c (as an ioctl) NtCreateFile : reply_open_pipe_and_X in smbd/pipes.c WaitNamedPipe : fully in dlls/kernel32/sync.c SetNamedPipeHandleState : partially in dlls/kernel32/sync.c GetNamedPipeHandleState : get_named_pipe_info in server/named_pipes.c TransactNamedPipe : partially as WriteFile then ReadFile ImpersonateNamedPipeClient: fully in dlls/kernel32/sync.c NtReadFile : TODO, in dlls/ntdll/file.c NtWriteFile : TODO, in dlls/ntdll/file.c CloseHandle : fd_close_handle through server/named_pipes.c (Exact details are omitted for brevity, in particular as the implementation of NamedPipes in Wine does not presently support MessageMode - at all - and thus requires designing almost from scratch. The other reason for brevity is the many layers of indirection, through "public" APIs, through kernel-level "private" APIs and finally in most instances to a call through to wineserver). Illustrated Example ------------------- To properly derive an understanding of NT Named Pipes, it helps to go through an illustrated example, outlining what happens over-the-wire, as it is the SMB traffic that gives the biggest clue as to what is needed. Take the following two programs, taken from the MSDN http://msdn.microsoft.com/en-us/library/aa365588(VS.85).aspx and http://msdn.microsoft.com/en-us/library/aa365592(VS.85).aspx respectively: Server Application: ------------------ #include #include #include #include #define BUFSIZE 4096 DWORD WINAPI InstanceThread(LPVOID); VOID GetAnswerToRequest(LPTSTR, LPTSTR, LPDWORD); int _tmain(VOID) { BOOL fConnected; DWORD dwThreadId; HANDLE hPipe, hThread; LPTSTR lpszPipename = TEXT("\\\\.\\pipe\\mynamedpipe"); // The main loop creates an instance of the named pipe and // then waits for a client to connect to it. When the client // connects, a thread is created to handle communications // with that client, and this loop is free to wait for the // next client connect request. _tprintf( TEXT("\nPipe Server: Awaiting initial client connection on %s\n"), lpszPipename); for (;;) { hPipe = CreateNamedPipe( lpszPipename, // pipe name PIPE_ACCESS_DUPLEX, // read/write access PIPE_TYPE_MESSAGE | // message type pipe PIPE_READMODE_MESSAGE | // message-read mode PIPE_WAIT, // blocking mode PIPE_UNLIMITED_INSTANCES, // max. instances BUFSIZE, // output buffer size BUFSIZE, // input buffer size 0, // client time-out NULL); // default security attribute if (hPipe == INVALID_HANDLE_VALUE) { _tprintf(TEXT("CreateNamedPipe failed, GLE=%d.\n"), GetLastError()); return -1; } // Wait for the client to connect; if it succeeds, // the function returns a nonzero value. If the function // returns zero, GetLastError returns ERROR_PIPE_CONNECTED. fConnected = ConnectNamedPipe(hPipe, NULL) ? TRUE : (GetLastError() == ERROR_PIPE_CONNECTED); if (fConnected) { printf("Client connected, creating a processing thread.\n"); // Create a thread for this client. hThread = CreateThread( NULL, // no security attribute 0, // default stack size InstanceThread, // thread proc (LPVOID) hPipe, // thread parameter 0, // not suspended &dwThreadId); // returns thread ID if (hThread == NULL) { _tprintf(TEXT("CreateThread failed, GLE=%d.\n"), GetLastError()); return -1; } else CloseHandle(hThread); } else // The client could not connect, so close the pipe. CloseHandle(hPipe); } return 0; } DWORD WINAPI InstanceThread(LPVOID lpvParam) // This is a thread processing function to read from and reply to a client // via the open pipe connection passed from the main loop. Note this allows // the main loop to continue executing, potentially creating more threads of // of this procedure to run concurrently, depending on the number of incoming // client connections. { HANDLE hHeap = GetProcessHeap(); TCHAR* pchRequest = (TCHAR*)HeapAlloc(hHeap, 0, BUFSIZE*sizeof(TCHAR)); TCHAR* pchReply = (TCHAR*)HeapAlloc(hHeap, 0, BUFSIZE*sizeof(TCHAR)); DWORD cbBytesRead = 0, cbReplyBytes = 0, cbWritten = 0; BOOL fSuccess = FALSE; HANDLE hPipe = NULL; // Do some extra error checking in a compound statement, this could be // made more specific for different recovery routines if more robust // recovery is needed. For this example we simply quit. if ( lpvParam == NULL || pchRequest == NULL || pchReply == NULL ) { printf( "\nERROR - Pipe Server Failure: InstanceThread has an " "unexpected NULL value.\n"); return 0; } // The thread's parameter is a handle to a pipe object instance. hPipe = (HANDLE) lpvParam; // Loop until done reading while (1) { // Read client requests from the pipe. fSuccess = ReadFile( hPipe, // handle to pipe pchRequest, // buffer to receive data BUFSIZE*sizeof(TCHAR), // size of buffer &cbBytesRead, // number of bytes read NULL); // not overlapped I/O if (! fSuccess || cbBytesRead == 0) break; GetAnswerToRequest(pchRequest, pchReply, &cbReplyBytes); // Write the reply to the pipe. fSuccess = WriteFile( hPipe, // handle to pipe pchReply, // buffer to write from cbReplyBytes, // number of bytes to write &cbWritten, // number of bytes written NULL); // not overlapped I/O if (! fSuccess || cbReplyBytes != cbWritten) break; } // Flush the pipe to allow the client to read the pipe's contents // before disconnecting. Then disconnect the pipe, and close the // handle to this pipe instance. FlushFileBuffers(hPipe); DisconnectNamedPipe(hPipe); CloseHandle(hPipe); HeapFree(hHeap, 0, pchRequest); HeapFree(hHeap, 0, pchReply); return 1; } VOID GetAnswerToRequest(LPTSTR pchRequest, LPTSTR pchReply, LPDWORD pchBytes) // This routine is a simple function to print the client request to the console // and populate the reply buffer with a default data string. { _tprintf( TEXT("Client Request String:\"%s\"\n"), pchRequest ); StringCchCopy( pchReply, BUFSIZE, TEXT("default answer from server") ); *pchBytes = (lstrlen(pchReply)+1)*sizeof(TCHAR); } Client Application: ------------------ #include #include #include #include #define BUFSIZE 512 int _tmain(int argc, TCHAR *argv[]) { HANDLE hPipe; LPTSTR lpvMessage=TEXT("Default message from client"); TCHAR chBuf[BUFSIZE]; BOOL fSuccess; DWORD cbRead, cbWritten, dwMode; LPTSTR lpszPipename = TEXT("\\\\.\\pipe\\mynamedpipe"); if( argc > 1 ) lpvMessage = argv[1]; // Try to open a named pipe; wait for it, if necessary. while (1) { hPipe = CreateFile( lpszPipename, // pipe name GENERIC_READ | // read and write access GENERIC_WRITE, 0, // no sharing NULL, // default security attributes OPEN_EXISTING, // opens existing pipe 0, // default attributes NULL); // no template file // Break if the pipe handle is valid. if (hPipe != INVALID_HANDLE_VALUE) break; // Exit if an error other than ERROR_PIPE_BUSY occurs. if (GetLastError() != ERROR_PIPE_BUSY) { printf("Could not open pipe"); return 0; } // All pipe instances are busy, so wait for 20 seconds. if (!WaitNamedPipe(lpszPipename, 20000)) { printf("Could not open pipe"); return 0; } } // The pipe connected; change to message-read mode. dwMode = PIPE_READMODE_MESSAGE; fSuccess = SetNamedPipeHandleState( hPipe, // pipe handle &dwMode, // new pipe mode NULL, // don't set maximum bytes NULL); // don't set maximum time if (!fSuccess) { printf("SetNamedPipeHandleState failed"); return 0; } // Send a message to the pipe server. fSuccess = WriteFile( hPipe, // pipe handle lpvMessage, // message (lstrlen(lpvMessage)+1)*sizeof(TCHAR), // message length &cbWritten, // bytes written NULL); // not overlapped if (!fSuccess) { printf("WriteFile failed"); return 0; } do { // Read from the pipe. fSuccess = ReadFile( hPipe, // pipe handle chBuf, // buffer to receive reply BUFSIZE*sizeof(TCHAR), // size of buffer &cbRead, // number of bytes read NULL); // not overlapped if (! fSuccess && GetLastError() != ERROR_MORE_DATA) break; _tprintf( TEXT("%s\n"), chBuf ); } while (!fSuccess); // repeat loop if ERROR_MORE_DATA _getch(); CloseHandle(hPipe); return 0; } First comes the CreateNamedPipe, which is an internal function allocating internal networking resources. No network traffic is observed. Next comes ConnectNamedPipe, the unix equivalent of "listen()". Again, no network traffic: the server blocks, waiting for incoming connections. Then the client can perform CreateFile(), which results in the first visible SMB network traffic; SMBopenX on an IPC$ (inter-process communication) share. Next comes WaitNamedPipe, which results in the next visible SMB packet. Internally, the server is informed, and ConnectNamedPipe thread is unblocked. The client is informed of the "unblock" by a return SMB reply. Next, the client OPTIONALLY changes the type of the pipe to "Message Mode" for reading purposes. It is an APPLICATION SPECIFIC decision to do this: the pipe can just as easily be left in "byte" mode, and the behaviour of the communication will be more akin to that of TCP streams than that of UDP datagrams. (Samba and Samba TNG do NOT support "byte" mode as their only use to date for Named Pipes has been as a conduit for MSRPC traffic, which always uses MessageMode). So, the client uses SetNamedPipeHandleState to tell the server to set up "queueing / blocking" mode, where individual "writes" must now be "atomic", encapsulated into a single "message". Internally, the recipient infrastructure must simply make a note of this requested behaviour, allocate resources if necessary, but otherwise do nothing to the actual server thread, which continues to block. The server is, at this time, now blocking in the InstanceThread, on ReadFile (and is also blocking on the main thread, waiting for another ConnectNamedPipe to succeed. We will assume that there are no other clients, and so will ignore the main thread from now on). Following the SetNamedPipeHandleState, the client performs a WriteFile, which results in the next SMB packet: an SMBwriteX. The server receives this request, wakes up the server thread which has been blocking on ReadFile, and a simple echo response is created (see GetAnswerToRequest). The client has already been notified, via an SMBwriteX response, that its write succeeded: it is now already blocking on ReadFile. The client has already caused an SMBreadX to be sent whilst the server is preparing its reply. The server thread's WriteFile fills internal buffers which hopefully are ready by the time that the SMBreadX is received. [note: "hopefully" is a note of hope for samba and samba tng, thanks to the decision to do a purely single-process-per-connection design. It's well known that samba's single-process architecture causes quite significant latency and other issues when it talks to a multi-threaded application, providing apparent good performance whilst actually blocking all but one of the client's threads. This is a long-standing issue, to be resolved by Samba]. The client receives the response to the SMBreadX, containing the Named Pipe response data from the server, and, in this instance, simply prints it out on-screen. After a key is pressed, the client closes the pipe, resulting in an SMBcloseX being sent; the server receives this close request; the server is notified that the number of connections has fallen to zero; thus it should no longer expect to receive any data, and the ReadFile call in which the server's listener-thread instance is blocking now returns with an error. Insights learned from the illustrated example ============================================= It makes a lot of sense to replicate this exact same procedure both _inside_ Wine and also as part of the communication between Wine and Samba / Samba TNG. In other words, a common protocol determined such that services in Wine or services in Samba or Samba TNG both talk the same language, and talk it in the same place. Applications, then, regardless of whether they be Wine NamedPipe Services or Samba or Samba TNG NamedPipe Services, can thus be talked to by Wine NamedPipe clients or Samba or Samba TNG NamedPipe clients, or any other type of client. By complete coincidence, Wine NamedPipe clients will also happen to be able to talk to Wine NamedPipe servers using the exact same protocol; Samba 3 NamedPipe clients will also be able to talk to Samba 3 NamedPipe servers using the exact same protocol; likewise for Samba 4, Samba TNG and any other system. In this regard, the emulated named pipes layer becomes very much "system calls" as if this was simple TCP/IP. Interoperability of the named pipes emulation layer is just a nice byproduct of its design. What makes the most sense ========================= It makes the most sense to create a mini SMB protocol. A "full" SMB protocol would result in quite a significant amount of infrastructure being pulled in to Wine or any other system which wishes to interoperate over Named Pipes. (Also, the use of MSRPC as the means to create the protocol also requires significant resources, and, given that the most significant use of Named Pipes is as an MSRPC transport, using MSRPC itself to encode emulated named pipes over which then MSRPC is proxied risks an infinite recursive loop. and just looks... odd). All packets, request and reply, will be of the following format: [uint32 length] sizeof data - Size of data fields transferred [int16 cmd] command - command field [int16 res] reserved - reserved data fields length bytes - varying data fields (see cmd) Note: the length field EXCLUDES the 4-byte length field, the 2-byte command field and the 2-byte reserved field itself, specifying purely the length of the data fields. Note: Intel word-order will be used for all 16-bit and 32-bit values. CreateNamedPipe --------------- Creating an emulated named pipe will involve exclusive atomic creation of a unix domain socket, named after the pipe on which the data is to be transferred using the mini SMB protocol. Technical issues involved are: * Exiting an application unexpectedly results in the inode being left lying around: it is therefore necessary to _delete_ the old unix domain socket filename. * It is not possible to simply randomly delete unix domain socket filenames as an application could still be using it! Therefore, exclusive locking is required. * Some unixen do not obey file permissions on create of unix domain sockets, therefore a subdirectory has to be created and the appropriate permissions set on the subdirectory. * Given that some applications (Samba, Samba TNG) will be (or already do) implementing security contexts, and the security contexts are used to perform the equivalent of unix "seteuid", and the security contexts are implicitly trusted, it is absolutely essential to lock out any unauthorised access to the unix domain socket subdirectory. * The implications for Wine, for example, are that for full security authentication semantics, wineserver must run as root - or an assistance program must run as root - and perform proxying to the protected subdirectory, after appropriate security and authentication checks. This is entirely application-specific and is not part of this specification: some implementors of this specification may choose to ignore security contexts entirely and/or implement security contexts at a later date. This combination of requirements plus technical limitations can be fulfilled as follows: * A subdirectory is created with protected permissions * A lock file named "lck." is created containing, for the sake of placing some content in it, the name of the pipe. The pipename, which will have given in UCS-2, will be converted to UTF-8. * The standard procedure for creating and locking unix "pid" files will be followed (see appendix for an example). * Failure to obtain an exclusive lock will result in an NT status code STATUS_OBJECT_NAME_EXISTS being returned, to indicate that the pipe has already been opened by another application. * Once a lock is established, a pipe named "pipe." is created in the protected subdirectory. The name of the pipe, which will have been given in UCS-2, will be converted to UTF-8. See appendix for example code that creates a protected unix domain socket. Note: "lck." is prepended to the lock file and "pipe." prepended to the pipe name in order to uniquely distinguish the two types of files, in the same subdirectory. Note: the buffer size and timeout parameters which are part of the "public" API are implementation-specific internal parameters that are NOT part over the npemu interoperability specification. Implementors must however still store, set and respect buffer sizes and the timeout period in internal data structures. CreateFile ---------- Named Pipes are actually authenticated: there is a security context associated with them. So, when a Named Pipe is created, it is necessary for the mini-SMB protocol to "catch up" so-to-speak with what's taken place so far. That includes the NetBIOS names (called and calling name); the Domain name and the actual user context: [uint32 length] sizeof data - variable-length [int16 cmd] 0x0000 - Create Named Pipe command [int16 res] reserved - reserved [UCS-2 pipe] \PIPE\ - name of pipe [UCS-2 nb_caller] MACHINENAME - NetBIOS caller (client) [UCS-2 nb_called] MACHINENAME - NetBIOS called (server) [UCS-2 domain] DOMAINNAME - Domain (or workstation) Name [uint32 ctx_len] length of context - length of SamInfo3 "blob" [netr_SamInfo3 *ctx] security context - Security context (SamInfo3) The length of the security context is specifically given so that implementations may choose to treat it as an opaque blob - instead of being forced to decode it. Early implementations and those implementations for whom security is unimportant may even choose to simply set the length of the security context to zero: for interoperability purposes, all implementations MUST anticipate this possibility and cope, accordingly, including returning STATUS_ACCESS_DENIED if they deem it appropriate. Additionally, implementations MAY choose to set the nb_caller, nb_called and domain names to length zero, for the purposes of connecting their own clients to their own servers, but should not expect full interoperability to be achievable with other implementations if they do so. nb_called name SHOULD be set to the same as the remote pipe destination, for example if the client wishes to create a pipe "\HIGHFIELD\pipe\lsarpc" then nb_called should be set to "HIGHFIELD". Reply: [uint32 length] sizeof data - set to 16 bytes. [int16 cmd] 0x0000 - Create Named Pipe reply [int16 res] reserved - reserved [uint32 handle] handle - file handle [uint32 timeout] default timeout - default timeout value, in msec. [uint32 status] NT status code - error code Note: the default timeout value is the value which the server application set using CreateNamedPipe. Having stored this value in application-specific private data structures inside the application-specific implementation, the default timeout value must now be returned to the client, for the client to store in *its* application-specific private data structures in *its* application-specific implementation, for later use (in WaitNamedPipe, if required). Note: the handle must be unique across all CreateFile calls per pipe. "Sharing" of the handle, between threads is possible, but TransactNamedPipe, ReadFile and WriteFile operations MUST be protected (atomic). Specifically, AT NO TIME must the transfer of data over the npemu unix domain socket be "shared". In Samba, this is not a problem: Samba's internal architecture is single-process. In Wine, special attention must be paid to ensure the integrity of the private data structures and of the data being transferred over the unix domain socket. WaitNamedPipe ------------- WaitNamedPipe is equivalent to api_WNPHS in Samba and Samba TNG. It is the function which causes ConnectNamedPipe to unblock and proceed. [uint32 length] sizeof data - set to 4 bytes. [int16 cmd] 0x0053 - WaitNamedPipe command [int16 res] reserved - reserved [uint32 handle] handle - file handle Reply: [uint32 length] sizeof data - set to 4 bytes. [int16 cmd] 0x0053 - WaitNamedPipe command [int16 res] reserved - reserved [uint32 status] NT status code - error code Note: the timeout field - of the "public" function - is NOT sent back to the server, but the implementation of WaitNamedPipe should use the value retrieved from the server (from the reply to CreateFile) if needed, should the implementation of WaitNamedPipe receive a value NMPWAIT_USE_DEFAULT_WAIT (0x00000000) as its timeout value to wait. SetNamedPipeHandleState ----------------------- SetNamedPipeHandleState changes the behaviour of the pipe, and is used by the client to indicate to the server its desire to operate in different modes. In particular, "Message Mode" can be set, which indicates that the server must queue individual writes as being unique and self-contained (similar to UDP Datagrams). The significant differences between UDP datagrams and "Message Mode" is that not only can a single message be obtained with multiple "Reads" and still have the message boundaries respected, but also "Message Mode" still respects the guaranteed reliability and the guaranteed message "order". In that regard, "Message Mode" has the best characteristics of both Streams and Datagrams. Given that CreateFile does not have room to specify either Message Mode or the Blocking Mode, SetNamedPipeHandleState is utilised instead. [uint32 length] sizeof data - set to 8 bytes. [int16 cmd] 0x0001 - SetNamedPipeHandleState command [int16 res] reserved - reserved [uint32 handle] handle - file handle [uint32 mode] mode - Message, Byte / Block, Nonblock. Reply: [uint32 length] sizeof data - set to 4 bytes. [int16 cmd] 0x0001 - SetNamedPipeHandleState command [int16 res] reserved - reserved [uint32 status] NT status code - error code Note: the public API of SetNamedPipeHandleState contains two other parameters - maxCollectionsCount and CollectDataTimeout. These are implementation-specific parameters that are consequently not transferred between npemu clients and servers. Implementors of NamedPipes must therefore store these two parameters in implementation-specific private data structures. Note: the mode values are exactly as specified in the MSDN and have the exact same purpose: PIPE_READMODE_BYTE 0x00000000 PIPE_READMODE_MESSAGE 0x00000002 PIPE_WAIT 0x00000000 PIPE_NOWAIT 0x00000001 TransactNamedPipe ----------------- This function is a time-saving and space-saving function which combines both write and read into one single function. Like ReadFile, it can return STATUS_MORE_PROCESSING_REQUIRED if, when the pipe is in MessageMode, there is still outstanding data yet to be read from the current message. In such circumstances, applications MUST follow up with repeated ReadFile calls until such time as the remaining data has been read. Also, TransactNamedPipe can be preceded by message-mode WriteFile calls. Under these circumstances, the TransactNamedPipe MUST be writing the LAST portion of the message. [uint32 length] sizeof data - variable length [int16 cmd] 0x0026 - TransactNamedPipe command [int16 res] reserved - reserved [uint32 handle] handle - file handle [int16 write_len] length of write - amount of data to be written [write_len bytes] data written - write_len bytes of write data [int16 read_len] length to read - amount of data to be read Reply: [uint32 length] sizeof data - variable length [int16 cmd] 0x0026 - TransactNamedPipe command [int16 res] reserved - reserved [uint32 status] NT status code - error code [int16 read_len] length of read - amount of data read [read_len bytes] data read - read_len bytes of data read Note: this operation MUST be an "atomic" operation, with no possibility for client threads to overlap in between updating of the internal data structures along with reading and writing. WriteFile --------- [uint32 length] sizeof data - variable length [int16 cmd] 0x002F - WriteFile command [int16 res] reserved - reserved [uint32 handle] handle - file handle [int16 mode] message flags - indicates e.g. start of message [int16 message_len] length of message - total amount of data expected [int16 write_len] length of write - amount of data to be written [write_len bytes] data written - write_len bytes of write data Reply: [uint32 length] sizeof data - 4 bytes. [int16 cmd] 0x002F - WriteFile command [int16 res] reserved - reserved [uint32 status] NT status code - error code Note: in MessageMode, on a new message, mode must be set to PIPE_RAW_MODE|PIPE_START_MESSAGE, and message_len set to the total length of the message expected to be transferred (which cannot exceed 64k according to the MSDN documentation). Subsequent writes can then be utilised to transfer the remaining data, with the last write, to complete the transfer of the last portion of the message, optionally being a TransactNamedPipe call. Note: in MessageMode, the infrastructure MUST double-check that a new message is only begun when the total amount of data previously written matches the message_len. It is important to bear in mind that TransactNamedPipe can finish off the write of a message. Note: updating of the internal data structures, to record the beginning of the new message, MUST be an atomic operation. If the application utilising the npemu layer allows multi-threading, the creation of a new message, and the storage of the message_len in application-specific private data structures, MUST be protected. Flags: PIPE_RAW_MODE 0x4 PIPE_START_MESSAGE 0x8 ReadFile -------- ReadFile must return STATUS_MORE_PROCESSING_REQUIRED if, when the pipe is in MessageMode, there is still outstanding data yet to be read of the current message. Implementations must keep track of the available data in application-specific private data structures. [uint32 length] sizeof data - 4 bytes. [int16 cmd] 0x002E - ReadFile command [int16 res] reserved - reserved [uint32 handle] handle - file handle Reply: [uint32 length] sizeof data - variable length [int16 cmd] 0x002E - ReadFile command [int16 res] reserved - reserved [uint32 status] NT status code - error code [int16 read_len] length of read - amount of data read [read_len bytes] data read - read_len bytes of data read Note: updating of the internal data structures, for example in MessageMode to record that data has been taken off of a message, MUST be an atomic operation. If the application utilising the npemu layer allows multi-threading, the subtraction of the data read from the application-specific private data structures MUST be protected with an exclusivity lock. CloseHandle ----------- This function is used client-side to indicate to servers that it no longer is connected to the emulated named pipe. Servers will then know to perform operations such as returing STATUS_PIPE_DISCONNECTED to any server threads that are blocking on ReadFile, for example. [uint32 length] sizeof data - 4 bytes. [int16 cmd] 0x0004 - CloseHandle command [int16 res] reserved - reserved [uint32 handle] handle - file handle Reply: [uint32 length] sizeof data - 4 bytes. [int16 cmd] 0x0004 - CloseHandle command [int16 res] reserved - reserved [uint32 status] NT status code - error code Note: CloseHandle on server-side pipes is an internal application-specific operation that results in closing of the unix domain socket, MUST involve unlocking of the fcntl, MAY result in deletion of the lock file and MAY result in deletion of the unix domain socket inode. The socket shutdown MUST occur BEFORE the unlock. Appendix: Data Structures / IDL =============================== UCS-2 ----- A null-terminated UCS-2 unicode string (identical to lsa_String from Samba PIDL files). length and size are in bytes (not "number of UCS-2 characters") and include the length of the 2-byte NULL termination. For example, a UCS-2 null-terminated string "HIGHFIELD\0" will result in length and size of "20" - not 9, 10 or 18. typedef struct { [value(2*strlen_m(string))] uint16 length; [value(2*strlen_m(string))] uint16 size; [size_is(size/2),length_is(length/2)] uint16 *string; } lsa_String; NET_USER_INFO_3 / netr_SamInfo3 ------------------------------- Here is the definition IDL, in Samba 4 "pidl" syntax, for netr_SamInfo3, aka the NET_USER_INFO_3 structure. samba 3 and samba 4 use these via pidl; samba tng contains a hand-marshalled implementation; with some tiny adaptation, the PIDL syntax can easily be converted to MIDL syntax and thus utilised by dceidl or WIDL.EXE. Decoding of the security context is *optional*, because implementations may choose to either ignore the security context, or may obtain it as an opaque type from other services: under these circumstances they may simply "pass it on" without actually ever needing to know what's in it, or communicate with more comprehensive (trusted) third party services for interpretation of the security context. For information on the NDR format, please see the OpenGroup's DCE/RPC specification for example here, on basic data types: http://www.opengroup.org/onlinepubs/9629399/chap14.htm#tagcjh_19_02 and here, on constructed data types: http://www.opengroup.org/onlinepubs/9629399/chap14.htm#tagcjh_19_03 typedef struct { [value(2*strlen_m(string))] uint16 length; [value(2*strlen_m_term(string))] uint16 size; [size_is(size/2),length_is(length/2)] uint16 *string; } lsa_StringLarge; typedef struct { [value(2*strlen_m(string))] uint16 length; [value(2*strlen_m(string))] uint16 size; [size_is(size/2),length_is(length/2)] uint16 *string; } lsa_String; typedef struct { uint32 rid; samr_GroupAttrs attributes; } samr_RidWithAttribute; typedef struct { uint32 count; [size_is(count)] samr_RidWithAttribute *rids; } samr_RidWithAttributeArray; typedef struct { uint8 sid_rev_num; /**< SID revision number */ [range(0,15)] int8 num_auths; /**< Number of sub-authorities */ uint8 id_auth[6]; /**< Identifier Authority */ uint32 sub_auths[15]; } dom_sid; typedef int64 NTTIME; typedef struct { uint8 key[16]; } netr_UserSessionKey; typedef struct { uint8 key[8]; } netr_LMSessionKey; /* Flags for user_flags below */ typedef { NETLOGON_GUEST = 0x00000001, NETLOGON_NOENCRYPTION = 0x00000002, NETLOGON_CACHED_ACCOUNT = 0x00000004, NETLOGON_USED_LM_PASSWORD = 0x00000008, NETLOGON_EXTRA_SIDS = 0x00000020, NETLOGON_SUBAUTH_SESSION_KEY = 0x00000040, NETLOGON_SERVER_TRUST_ACCOUNT = 0x00000080, NETLOGON_NTLMV2_ENABLED = 0x00000100, NETLOGON_RESOURCE_GROUPS = 0x00000200, NETLOGON_PROFILE_PATH_RETURNED = 0x00000400, NETLOGON_GRACE_LOGON = 0x01000000 } netr_UserFlags; /* account control (acct_flags) bits */ typedef { ACB_DISABLED = 0x00000001, /* 1 = User account disabled */ ACB_HOMDIRREQ = 0x00000002, /* 1 = Home directory required */ ACB_PWNOTREQ = 0x00000004, /* 1 = User password not required */ ACB_TEMPDUP = 0x00000008, /* 1 = Temporary duplicate account */ ACB_NORMAL = 0x00000010, /* 1 = Normal user account */ ACB_MNS = 0x00000020, /* 1 = MNS logon user account */ ACB_DOMTRUST = 0x00000040, /* 1 = Interdomain trust account */ ACB_WSTRUST = 0x00000080, /* 1 = Workstation trust account */ ACB_SVRTRUST = 0x00000100, /* 1 = Server trust account */ ACB_PWNOEXP = 0x00000200, /* 1 = User password does not expire */ ACB_AUTOLOCK = 0x00000400, /* 1 = Account auto locked */ ACB_ENC_TXT_PWD_ALLOWED = 0x00000800, /* 1 = Encryped text password is allowed */ ACB_SMARTCARD_REQUIRED = 0x00001000, /* 1 = Smart Card required */ ACB_TRUSTED_FOR_DELEGATION = 0x00002000, /* 1 = Trusted for Delegation */ ACB_NOT_DELEGATED = 0x00004000, /* 1 = Not delegated */ ACB_USE_DES_KEY_ONLY = 0x00008000, /* 1 = Use DES key only */ ACB_DONT_REQUIRE_PREAUTH = 0x00010000, /* 1 = Preauth not required */ ACB_PW_EXPIRED = 0x00020000, /* 1 = Password Expired */ ACB_NO_AUTH_DATA_REQD = 0x00080000 /* 1 = No authorization data required */ } samr_AcctFlags; typedef struct { NTTIME last_logon; NTTIME last_logoff; NTTIME acct_expiry; NTTIME last_password_change; NTTIME allow_password_change; NTTIME force_password_change; lsa_String account_name; lsa_String full_name; lsa_String logon_script; lsa_String profile_path; lsa_String home_directory; lsa_String home_drive; uint16 logon_count; uint16 bad_password_count; uint32 rid; uint32 primary_gid; samr_RidWithAttributeArray groups; uint32 user_flags; netr_UserSessionKey key; lsa_StringLarge logon_server; lsa_StringLarge domain; dom_sid2 *domain_sid; netr_LMSessionKey LMSessKey; uint32 acct_flags; uint32 unknown[7]; } netr_SamBaseInfo; typedef struct { netr_SamBaseInfo base; } netr_SamInfo2; typedef struct { dom_sid2 *sid; samr_GroupAttrs attributes; } netr_SidAttr; typedef struct { netr_SamBaseInfo base; uint32 sidcount; [size_is(sidcount)] netr_SidAttr *sids; } netr_SamInfo3; fcntl locking example --------------------- This is example code showing how exclusive locking can be performed across multiple applications, on POSIX compliant systems. Use of such fcntl locking is a mandatory requirement, for the protection of the creation of named pipes. #include #include #include #include #ifndef O_NONBLOCK #define O_NONBLOCK 0 #endif int fcntl_lock(int fd, int op, off_t offset, off_t count, int type) { struct flock lock; int ret; printf("fcntl_lock %d %d %.0f %.0f %d\n", fd, op, (double)offset, (double)count, type); lock.l_type = type; lock.l_whence = SEEK_SET; lock.l_start = offset; lock.l_len = count; lock.l_pid = 0; errno = 0; ret = fcntl(fd, op, &lock); if (errno != 0) { fprintf(stderr, "fcntl_lock: fcntl lock gave errno %d\n", errno); } /* a lock query */ if (op == F_GETLK) { if ((ret != -1) && (lock.l_type != F_UNLCK) && (lock.l_pid != 0) && (lock.l_pid != getpid())) { printf("fcntl_lock: fd %d is locked by pid %d\n", fd, (int)lock.l_pid); return (1); } /* it must be not locked or locked by me */ return (0); } /* a lock set or unset */ if (ret == -1) { fprintf(stderr, "fcntl_lock: lock failed at offset %.0f count %.0f op %d type %d (%d)\n", (double)offset, (double)count, op, type, errno); return (0); } /* everything went OK */ printf("fcntl_lock: Lock call successful\n"); return (1); } /** * return the pid in a pidfile. return 0 if the process (or pidfile) * does not exist */ pid_t pidfile_pid(const char *name) { int fd; char pidstr[20]; unsigned ret; char pidFile[128]; sprintf(pidFile, "%s/%s.pid", "/tmp", name); fd = open(pidFile, O_NONBLOCK | O_RDWR, 0644); if (fd == -1) { return 0; } if (read(fd, pidstr, sizeof(pidstr)-1) <= 0) { goto ok; } ret = atoi(pidstr); printf("pid: %d\n", ret); if (fcntl_lock(fd,F_SETLK,0,1,F_WRLCK)) { /* we could get the lock - it can't be a Samba process */ goto ok; } close(fd); return (pid_t)ret; ok: close(fd); unlink(pidFile); return 0; } /* create a pid file in the lock directory. open it and leave it locked */ void pidfile_create(const char *name) { int fd; char buf[20]; size_t writelen; char pidFile[128]; pid_t pid; sprintf(pidFile, "%s/%s.pid", "/tmp", name); pid = pidfile_pid(name); if (pid != 0) { fprintf(stderr, "ERROR: %s is already running. File %s exists and process id %d is running.\n", name, pidFile, (int)pid); exit(1); } fd = open(pidFile, O_NONBLOCK | O_CREAT | O_WRONLY | O_EXCL, 0644); if (fd == -1) { fprintf(stderr, "ERROR: can't open %s: Error was %d\n", pidFile, errno); exit(1); } if (fcntl_lock(fd, F_SETLK,0,1,F_WRLCK)==0) { fprintf(stderr, "ERROR: %s : fcntl lock of file %s failed. Error was %d\n", name, pidFile, errno); exit(1); } /* * writelen calculation gracefully ignores the possibility of * a buffer overrun in sprintf */ writelen = sprintf(buf, "%u\n", (unsigned int) getpid()); if (write(fd, buf, writelen) != writelen) { fprintf(stderr, "ERROR: can't write to file %s: %d\n", pidFile, errno); exit(1); } /* Leave pid file open & locked for the duration... */ } /* run this twice. first time succeeds, second time throws a wobbly. */ int main(int argc, char *argv[]) { pidfile_create("test"); sleep(5000); } Opening of Unix Domain Sockets ------------------------------ Here is example code for the opening of a unix domain socket, for use by client applications. int open_pipe_sock(char *path) { int sock; struct sockaddr_un sa; sock = socket(AF_UNIX, SOCK_STREAM, 0); if (sock < 0) { printf("unix socket open failed\n"); return sock; } ZERO_STRUCT(sa); sa.sun_family = AF_UNIX; strcpy(sa.sun_path, path); printf("socket open succeeded. file name: %s\n", sa.sun_path); if (connect(sock, (struct sockaddr *)&sa, sizeof(sa)) < 0) { printf("socket connect to %s failed\n", sa.sun_path); close(sock); return -1; } return sock; } Protected Creation of Unix Domain Sockets ----------------------------------------- Here is example code for the protected creation of unix domain sockets, for use by server applications. Also included is code that will allow recovery from unexpected termination of a server application, by deleting an old sockaddr_un. An exclusive Lock is STILL required PRIOR to creation of the Unix Domain Socket. For a better example where exclusive root-only access is to be used, see Samba's source code winbindd.c. The code below has the advantage of being Public Domain. For direct equivalence with the same Samba source code in winbindd.c set dir_perms to \0755 and path_perms to \0600, but do NOT expect the path perms to be respected on all unixen (Solaris is known to definitely ignore chmod on unix domain sockets). int create_pipe_socket(const char *dir, int dir_perms, const char *path, int path_perms) { int s; struct sockaddr_un sa; struct stat sbuf; printf("create_pipe_socket: %s 0%o %s 0%o\n", dir, dir_perms, path, path_perms); mkdir(dir, dir_perms); if (lstat(dir, &sbuf) != 0 || !S_ISDIR(sbuf.st_mode)) { printf("Error setting up pipe dir %s\n", path); return -1; } if (chmod(dir, dir_perms) < 0) { printf("chmod on %s failed\n", dir); return -1; } if (remove(path)!=0 && errno != ENOENT) { printf("remove on %s failed\n", path); } /* start listening on unix socket */ s = socket(AF_UNIX, SOCK_STREAM, 0); if (s < 0) { printf("socket open failed\n"); return -1; } ZERO_STRUCT(sa); sa.sun_family = AF_UNIX; strcpy(sa.sun_path, path); if (bind(s, (struct sockaddr *)&sa, sizeof(sa)) < 0) { printf("socket bind to %s failed\n", sa.sun_path); close(s); remove(path); return -1; } if (s == -1) { printf("bind failed\n"); remove(path); return -1; } if (path_perms != 0) { chmod(path, path_perms); } if (listen(s, 5) == -1) { printf("listen failed\n"); return -1; } printf("unix socket opened: %s\n", path); return s; }