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.