Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles / Languages / C#

C#/.NET Network File System (NFS) Server

4.86/5 (11 votes)
3 Jan 2012CPOL12 min read 80K   3.2K  
A basic implementation of an NFS server in C#.

Sample Image

Introduction

This article provides a basic description of an NFS server implemented in C# 1.0. This was written about 8 years ago to address a problem present at the time and also as a vehicle to learn C#. This means none of the collections are generic based plus it's beginner code, so please don't be too critical. It works and has been used in personal production mode though it can get into an erroneous state which can presently only be fixed by restarting the process. Importantly, it supports the creation and use of UNIX Symbolic Links. It does not implement any form of security and is limited to version 2 of NFS and only running over UDP. As such it should be considered a prototype but if there is a need to access a Windows machine via NFS, then it may form a stop-gap solution.

It was quite a long time ago that the research was performed and the code written. Therefore a lot of this article is based on what I can remember and from a quick perusal of the source.

The reason I'm writing this article and making the code Open Source is that recently I had reason to review the NFS Server project. It more or less works and there didn't seem to be a C# Open Source NFS Server available.

Background

I was working on a cross-platform (Windows and many flavours of UNIX) application. The primary development environment was Windows but all the code needed to be compiled and tested on UNIX. The easiest way to accomplish this was to use Linux running on VMWare hosted on a Windows machine. This would allow the virtual Linux machine to access the source code on the primary Windows machine using SAMBA to access a SMB/CIFS share. Unfortunately the build environment for UNIX created symbolic links. These were not supported by either the SAMBA client or SMB. This left three alternatives. Firstly, host the source on Linux and use SAMBA in server mode to share it. This didn't appeal as 8 years ago the resources on the Windows machine weren't sufficient to keep a background VM running all the time. Secondly, a 3rd party NFS server could be used but one wasn't available, at least for a small cost. The final option was to write one, plus it was also a meaty project to test out my C# skills.

Using the code

The solution contains the following binaries:

  • nfs.exe
  • mountV1.dll
  • nfsV2.dll
  • portmapperV1.dll
  • RPCV2Lib.dll

The NFS server is started by running nfs.exe. This needs all the other DLLs to be present. This initiates the three daemons required to provide NFS support:

  • Portmapper
  • As NFS is built on top of SunRPC, this is a generic service an RPC client initially connects to in order to discover what protocol the requested RPC service is available on (TPC and/or UDP) and if available what port number to use.

  • mountd
  • For some reason, the actual request to initially access the remote file system is made to and handled by a separate process from the rest of the NFS system which handles all the other requests except unmount. mountd is this process.

  • nfsd
  • This is the main NFS server process. Once mounted, this handles all requests to do with the remote file system, i.e., list directories, opening and closing files, accessing their contents, etc.

The ports used are: 111, 635 and 2049 respectively. The ones for portmapper (111) and nfsd (2049) are the standard assigned numbers. The 635 for mountd should be 645 to follow convention. As this value is requested from the portmapper then it doesn't pose a problem. Currently these values are hard-coded as constants within the source.

Rather than having to start three separate processes inetd nfs.exe starts all three. These are not separate processes but instead separate threads. Therefore stopping nfs.exe also stops the portmapper and mountd services in addition to the NFS server. To turn this into production quality, this would become a service or three.

To use from Linux, the following command is required:

mount -t nfs -o proto=udp -vers=2 <host>:<dir> <mountpoint>

E.g.:

mount -t nfs -o proto=udp -vers=2 192.168.0.1:C:/users/somebody/foo /foo

The case is irrelevant for the Windows path but it is relevant for the UNIX mount point. A hostname can be used in place of the IP address. To unmount the file system on the UNIX machine, use:

umount <mountpoint>

E.g.:

umount /foo

The server supports the following operations:

  • Listing files and directories
  • Creating and deleting files
  • Creating and deleting directories
  • Renaming files and directories
  • Reading and writing files
  • Creating, deleting, and using symbolic links
  • Obtaining file-system info, e.g., free disk space via df -k
  • Obtaining file info, e.g., ls -l

Note: It's around directory handling where nfs.exe seems to get upset. Also, speed isn't that great. The screenshot was taken on Linux and saved to a local file system. It was then copied to Windows via NFS. It's around 280K and took a few seconds to copy.

The main point of creating the server was to allow the creation and manipulation of Symbolic Links from UNIX to Windows. These are implemented by the NFS server creating <file>.sl files on Windows. These contain a path to the actual file. When an operation is performed on the Symbolic Link, the NFS server applies it against the file referenced in the '.sl' file apart from deletion which applies to the Symbolic Link.

Files and directories can be created, deleted, and manipulated on both sides with changes reflected on the other. This includes creating '.sl' files on Windows by hand.

A quick look at the code

The solution contains six sections of which one is a solution folder containing the PDFs of the relevant RFCs. There are four DLL projects and finally the NFS project that brings them all together.

RPCV2Lib

The three daemons are all implemented as SunRPC servers. This library provides the common Remote Procedure Call (RPC) infrastructure which includes creating an endpoint, listening for incoming requests, performing preliminary handling, and finally the subsequent dispatch to a dedicated handler. Handling a request is daemon specific which is why each daemon derives from rpcd which forms part of the library.

Handling a Remote Procedure Call is essentially the task of de-marshalling the incoming data, performing the request, and returning a result which needs to be marshaled. The call is in the form of a SunRPC request which itself is formatted using XDR; which is neutral format for data serialization. The initial de-marshalling is common and is handled within rpcd using the the CrackRPC method. Once the request has passed validity tests, the Proc virtual method is invoked which is implemented by the respective daemon to handle the call specifically.

Once the initial handling is complete, RPCV2Lib is still used as the specific handler needs to de-marshal the remaining data. The initial handling is just validation and obtaining the RPC number which is required for further dispatch. All the RPC/XDR de-marshalling code is provided by this library. When the Proc method is invoked, it is passed an identifier corresponding to the actual RPC, an instance of rpcCracker which is used to perform the de-marshalling. The other thing that's passed is an instance of rpcPacker. This is used to marshal the response to the call.

portmapperV1

This is fairly simple. It just implements a single RPC that returns the port numbers for mountd and nfsd. Each of these is obtained by separate requests. The caller requests the port number for a specific version and protocol. In the case of this implementation, these must be UDP and version 1 of mount protocol and version 2 of NFS. Requesting anything else results in an error response.

mountV1

This implements Mount and Unmount RPCs.

nfsV2

This is where the majority of the RPCs are implemented. As mentioned, this handles all the file and directory operations other than mounting and unmounting.

nfs

This is a straightforward executable. It depends upon the portmapperV1, mountV1, and nfsV2 projects. It simply creates an instance of each, spawns a thread for each, and sets them going, each sitting on their own UDP port waiting for incoming RPCs. It then sits and waits for them all to finish. This they never do unless an unhandled exception slips through as they are all in infinite loops. Ctrl-C in the Console window is the usual way to terminate them!

Handles

What hasn't been mentioned so far are the contents of the fileHandle.cs file. This contains the FileTable class. This is mainly composed of static methods though the constructor isn't, hence making it a kind of a singleton; nasty! That aside, this binds mountd and nfsd together. An (the) instance of this is created by nfs which combined with the static methods makes it accessible to the other daemons, in particular mountd.

I can't remember the exact details but along with looking at the code, this table contains entries for any files and directories that are currently in use. In use could mean being open etc., and for directories currently being the working directory. Once an entry for the path has been added to the table, a file handle which is a unique ID (and can be used to map to a UNIX inode) is created. This is used to identify the file or directory in all subsequent RPCs. The path is only needed when first accessing the file or directory. The class provides the ability to obtain a handle from the path name and vice versa depending on the type of operation being performed, e.g., a 'list' operation returns a list of handles but if the client needs to display readable names, then it will request the names associated with the handles.

I don't think entries are ever removed from the FileTable well, the underlying instance of HandleTable that contains the mapping, unless an actual file or directory is deleted. If a file is closed, then the entry remains which is potentially a problem as the table could grow until all the memory is consumed.

Looking at the RPCs implemented in nfsd.cs, what's odd is that there is no open or close method. Instead, if I remember correctly, the Lookup RPC is used to discover if a file or directory has been opened and if it hasn't, then do so. It is at this point that an entry is added to the FileTable. I think that each operation on a file should result in the underlying file being opened, the operation performed, and then closed, hence there is no explicit close method in the NFS specification as all operations leave the file closed. This leads to an obvious performance issue with the same file potentially being opened and closed many times for successive operations. I think that any optimizations are thus an implementation detail of the server rather than described in the NFS specification.

The reason for this is that NFS (or at least in version 2) is a stateless protocol so the server does not need to maintain the state of any files. This is the responsibility of the client. This is also where the unique ID is important as this is what the client associates state with. Also, NFS having a UNIX background, and as briefly mentioned, each UNIX file having a unique ID due from the i-number member of the i-node structure, makes it easier to implement an NFS client.

The unique ID was a problem for the C# implementation as a unique ID of the correct format could not be obtained from NTFS. Instead, the HandleTable provides a mapping from the generated ID to a path name when first accessed. These were generated on a per-session basis which is why a file or directory was added to the FileTable isn't removed unless the entity was deleted or an error occurred as it must remain the same for the entire session.

How was it built

I started by reading the appropriate RFCs:

The actual implementation began with portmapper which then required understanding RPCs and implementing basic XDR marshalling and de-marshalling. Once a file-system could be mounted, then the file and directory listing functionality was added and so on. One of the easier aspects of implementing an RFC is that you're programming to a specification. You still have to design the implementation architecture but unlike some projects, some of the hard design work has been done for you. Thank you IETF!

It wasn't all plain sailing especially around the XDR encoding. It's very easy to make a mistake there, and if I remember correctly, despite the specifications, some NFS clients didn't behave as they should. This is where Ethereal proved invaluable. This is a free network sniffer and rather than just dumping at the TCP/IP level, it understands various higher level protocols including NFS. This meant I could sniff the conversations between existing NFS servers and clients and compare the dumps with my server and see where the problem was. I don't think I would have got this far without it.

Is this still needed?

NTFS now supports Junction Points which enable Symbolic Links to be created on NTFS. As such it seems possible that this functionality maybe possible when mounting an NTFS file system on Linux via smbmount. A quick investigation reveals this not to be the case, though hard links could be created. On further investigation, these appeared to be implemented by just copying the original file on the mounted NTFS.

Summary

That's basically it. A more or less working implementation of NFS V2 over UDP with no security and a few bugs and a lot of potential for refactoring. It might offer a short-term solution if Symbolic Links are needed when mounting NTFS from UNIX. Also, it could act as a reference to anyone wanting to build a more complete and/or up to date NFS server.

All the source code and binaries are provided as accompanying zips. The source code is Open Source and is available on GitHub as https://github.com/petebarber/NFS.

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)