InfoArp: Invocar al Api Win32, crear un servidor TCP y montar un servicio de windows con .Net

Hola!


Tal como os había prometido, aquí teneis un primer post que os cuenta cómo se realizó InfoArp. Un pequeño proyecto en C# que invoca a funciones del API Win32, crea un servidor TCP y monta un servicio de windows.


Motivación
En mi trabajo se precisaba de un servicio de Windows que devolviese la tabla ARP (normalmente MACs asociadas a IPs) del propio servidor. Esta información podría obtenerse con SNMP pero fue un encargo por compatibilidad con un software de monitorización propio.
Los aspectos interesantes del proyecto son:



  • ¿Cómo consulto la tabla ARP de un equipo desde .NET?

  • ¿Cómo monto un server TCP desde .NET?

  • ¿Cómo hago que se ejecute en un servicio de Windows?

Vista general
En este diagrama de clases estático vemos la disposición general del proyecto. Para la consulta de la tabla ARP tenemos una clase adaptador, ArpAdapter, su método getArpTable() es invocado por  InfoArpServer que actúa de server TCP esperando conexiones entrantes en un puerto configurable. InfoArpService es la clase que implementa el servicio de Windows, lanza desde el método OnStart a InfoArpServer como thread. La clase ArpInfoServiceInstaller es necesaria para el instalador del servicio.



Obtener la tabla ARP desde .NET
El framework 1.1 carece de métodos o clases para ello. Iván González me sugirió que podría haber algo en la parte de Windows  Instrumentation, y aunque descubrí un montón de cosas, tampoco encontramos nada.  Al final, en el bendito MSDN, descubrí una función del API de win32 llamada GetIpNetTable en IP Helper functions (Win32 and Com development), que se encuentra en la dll del sistema IpHlpApi.dll. El siguiente paso a resolver fue cómo invocarla desde C#.
La invocación a funciones de dlls (win32, com, etc.) fuera del framework implica la ejecución del código no administrado. Los tipos de datos de entrada y de salida no se corresponden a ningún tipo común del framework.  Hay que hacer un marshalling y un unmarshalling de los parámetros de entrada y salida. Fue un agradable flashback a la programación en C y C++.  Gracias a la ayuda de Nicholas Paldino (.NET, C# MVP) y las clases de System.Runtime.InteropServices el trabajo se simplificó mucho. La clase ArpAdapter resuelve los problemas anteriormente mencionados.
Para referenciar a una función del API de Win32 no es necesario referenciar la DLL en nuestro proyecto basta con incluir en una clase:


[DllImport(«IpHlpApi.dll»)]
[return: MarshalAs(UnmanagedType.U4)]
private static extern int GetIpNetTable( IntPtr pIpNetTable,
 [MarshalAs(UnmanagedType.U4)] ref int pdwSize, bool bOrder);


Fijaos que GetIpNetTable tiene un puntero a una tabla con las entradas ARP. Es necesario conocer a priori el tamaño de la tabla para poder recuperarla. Esto se hace con el siguiente código:


public MIB_IPNETROW[] getArpTable(bool sorted)
{   
 // The table
 MIB_IPNETROW[] table = null;


 // The number of bytes needed.
 int bytesNeeded = 0;


 // The result from the API call.
 int result = GetIpNetTable(IntPtr.Zero, ref bytesNeeded, sorted);


 // Call the function, expecting an insufficient buffer.
 if (result != ERROR_INSUFFICIENT_BUFFER)
 {
  // Throw an exception.
  throw new Win32Exception(result);
 }
   
 // Allocate the memory, do it in a try/finally block, to ensure
 // that it is released.
IntPtr buffer = IntPtr.Zero;


 try
 {
  // Allocate the memory.
  buffer = Marshal.AllocCoTaskMem(bytesNeeded);


  // Make the call again. If it did not succeed, then
  // raise an error.
  result = GetIpNetTable(buffer, ref bytesNeeded, false);


  // If the result is not 0 (no error), then throw an exception.
  if (result != 0)
  {
   // Throw an exception.
   throw new Win32Exception(result);
  }


  // Now we have the buffer, we have to marshal it. We can read
  // the first 4 bytes to get the length of the buffer.
  int entries = Marshal.ReadInt32(buffer);
 
  // Increment the memory pointer by the size of the int.
  IntPtr currentBuffer = new IntPtr(buffer.ToInt64() +      System.Runtime.InteropServices.Marshal.SizeOf(typeof(int)));


  // Allocate an array of entries.
  table = new MIB_IPNETROW[entries];


  // Cycle through the entries.
  for (int index = 0; index < entries; index++)
  {
   // Call PtrToStructure, getting the structure information.
   table[index] = (MIB_IPNETROW) Marshal.PtrToStructure(new
    IntPtr(currentBuffer.ToInt64() + (index *
    Marshal.SizeOf(typeof(MIB_IPNETROW)))),
     typeof(MIB_IPNETROW));
  }
}
 finally
 {
  // Release the memory.
  Marshal.FreeCoTaskMem(buffer);
}
 return table;
}


Puede parecer complicado pero una vez mostrado el camino, si lo estudiáis con algo de detenimiento, veréis que no hace nada extraño.  Una vez que tenemos resuelto el primer problema, vamos a por el  segundo: el servidor TCP.


Servidor TCP
El servidor TCP es trivial. Está basado en el código de servidor TCP simple de hora y fecha de “How I do… “ de gotdot.net.
Ojo, este código es extremadamente simple y no es válido para todos los servidores  TCP.  Fijaos que lanza un único TCPListener que atiende la petición, lo que implica que solo se puede atender a un cliente simultáneamente. Si se recibiese otra petición, ésta tendría que esperar a que se resolviese la anterior.  Puesto que  no hay diálogo entre el cliente y el servidor  y la información a devolver es pequeña uno puede permitirse esta licencia. Sin embargo, en un entorno con diálogo entre cliente y servidor y múltiples peticiones concurrentes, habría que crear un pool de threads con TCPListeners para atender nuevas conexiones mientras se resuelven otras.


TcpListener tcpl = new TcpListener(serverAdress, port);
tcpl.Start();
   
while (true)
{
 // Accept will block until someone connects
 Socket s = tcpl.AcceptSocket();
  
 // Security check
 if (restrictedClientAddress ==
((IPEndPoint)s.RemoteEndPoint).Address)
 {     
  // Convert the string to a Byte Array and send it
  Byte[] byteDateLine = ASCII.GetBytes( getArpTable(
outputFormat).ToCharArray());
  s.Send(byteDateLine, byteDateLine.Length, 0);     
 }        
 s.Close();
}


Se crea un TcpListener  en una IP del servidor (0.0.0.0 si queréis cualquier interface) y un puerto.  A continuación entra en un bucle infinito aceptando conexiones entrantes. En nuestro caso, por añadirle algo de seguridad, solo se contestan las peticiones de una IP determinada.  Observad que podemos comparar directamente las IPs porque IPAddress implementa equals. Acto seguido se construye el buffer de envío y se manda al cliente.


Crear un servicio de Windows.
Realmente fácil. Con Visual Studio simplemente tenéis que crear un proyecto de Servicio de Windows. En nuestro caso la clase del servicio se llama InfoArpService y la plantilla de VS ya nos evita teclear bastante código.
Lo primero es darle un nombre al servicio:


private void InitializeComponent()
{
 components = new Container();
 this.ServiceName = «InfoArp»;
}


Lo segundo es implementar los métodos que responden a los eventos de inicio (OnStart) y parada (OnStop) del servicio. Se invoca un método privado local, ExecuteMain(), como thread.


public InfoArpService()
{
 InitializeComponent();   
 serverThread = new Thread(new ThreadStart(ExecuteMain));
 serverThread.IsBackground = true;
}


/// <summary>
/// Run the service
/// </summary>
/// <param name=»args»>arguments</param>
protected override void OnStart(string[] args)
{
 serverThread.Start();
}
 
/// <summary>
/// Stop the service
/// </summary>
protected override void OnStop()
{
 serverThread.Interrupt();
}


El método ExecuteMain() debe interceptar la excepción  ThreadInterruptedException que se recibe cuando se desea parar el servicio. Aunque parezca extraño es la forma correcta de finalizar un thread externamente.



private void ExecuteMain()
{
 try
 {   
  InfoArpServer.Launch(
   IPAddress.Parse( ConfigurationSettings.AppSettings[«ServerIP»]),
   Int32.Parse( ConfigurationSettings.AppSettings[«ServerPort»] ),
IPAddress.Parse(  ConfigurationSettings.AppSettings[«RestrictedClientIP»]),
   ConfigurationSettings.AppSettings[«ArpOutputFormat»]);
 }
 catch (ThreadInterruptedException)
 {
  // Simply exit.
 }
 catch(ConfigurationException ce)
 {
  EventLog.WriteEntry(
String.Format(«Can’t read configuration. Service stopped. n {0}»,
     ce.Message), EventLogEntryType.Error);
 }
 catch(Exception e)
 {
  EventLog.WriteEntry(
String.Format(«Unexpected exception. Service stopped.n {0}n{1}»,
    e.Message, e.StackTrace), EventLogEntryType.Error);
 } 
}



Las excepciones no esperadas y las derivadas de la configuración son registradas en el registro de sucesos.
También es necesario crear una clase instaladora, ArpInfoServiceInstaller, imprescindible para el proceso de instalación de nuestro servicio.


[RunInstallerAttribute(true)]
public class ArpInfoServiceInstaller : Installer
{
 private ServiceInstaller serviceInstaller1;
 private ServiceProcessInstaller processInstaller;


 public ArpInfoServiceInstaller()
 {
  // Instantiate installers for process and services.
  serviceInstaller1 = new ServiceInstaller();
  processInstaller = new ServiceProcessInstaller();


  // The services are started automatically.
  serviceInstaller1.StartType = ServiceStartMode.Automatic;


  // The service run under the system account.
  processInstaller.Account = ServiceAccount.LocalSystem;


  // ServiceName must equal those on ServiceBase derived classes.           
  serviceInstaller1.ServiceName = «InfoArp»;


  // Add installers to collection. Order is not important.
  Installers.Add(serviceInstaller1);
  Installers.Add(processInstaller);


   }
}
Se indica el modo de arranque por defecto (manual, automático, …) y como quién se va a ejecutar. Es importante que el nombre del servicio coincida con el definido en InfoArpService.


Instalación
Generamos la solución en release y abrimos la carpeta binRelease de nuestro proyecto. Deberíamos encontrar un ejecutable y el archivo de configuración que modificaremos según nuestro entorno.
Para instalar el servicio tenemos que ejecutar la utilidad installutil que viene con el framework. Desde la línea de comandos hacemos:



C:windowsMicrosoft.NETFrameworkv1.1.4322installutil InfoArpService.exe


Generará diversos archivos de log además de la salida por la consola.


Ejecutando una instalación de transacción.
Iniciando la fase de instalación dentro de la instalación.
Consulte el contenido del archivo de registro sobre el progreso del ensamblado binreleaseinfoarpservice.exe.
El archivo está ubicado en binreleaseinfoarpservice.InstallLog.
Instalando ensamblado ‘binreleaseinfoarpservice.exe’.
Los parámetros afectados son:
   assemblypath = binreleaseinfoarpservice.exe
   logfile = binreleaseinfoarpservice.InstallLog
Instalando el servicio InfoArp…
El servicio InfoArp se ha instalado correctamente.
Creando el origen de EventLog InfoArp en el registro Application…
La fase de instalación finalizó correctamente y la fase de confirmación está empezando.
Consulte el contenido del archivo de registro sobre el progreso del ensamblado binreleaseinfoarpservice.exe.
El archivo está ubicado en binreleaseinfoarpservice.InstallLog.
Confirmando ensamblado ‘binreleaseinfoarpservice.exe’.
Los parámetros afectados son:
   assemblypath = binreleaseinfoarpservice.exe
   logfile = binreleaseinfoarpservice.InstallLog


Si vamos al inicio > panel de control > herramientas administrativas > servicios veremos que ha añadido nuestro servicio. Inicialmente está parado y se ejecutará en el próximo inicio del sistema. Si lo queremos lanzar inmediatamente haremos clic sobre él y clic en el botón iniciar del cuadro de diálogo de propiedades del servicio.
Para desinstalar el servicio tendremos que ejecutar  installutil con las opción /u.  Si el servicio está corriendo intentará pararlo primero.


C:windowsMicrosoft.NETFrameworkv1.1.4322installutil /u InfoArpService.exe


Para probar el servicio haced un telnet a la IP especificada del servidor  y puerto correspondiente. En el archivo de configuración también podéis especificar el formato de salida.


C:>telnet 193.144.53.xx 3333


ARP 193.144.53.1x 00:0A:AF:xx:xx:xx
ARP 193.144.53.1x 00:D0:95:xx:xx:xx
ARP 193.144.53.x4 00:03:E8:xx:xx:xx
ARP 193.144.53.2x 00:01:02:xx:xx:xx
ARP 193.144.53.x1 00:04:75:xx:xx:xx
ARP 193.144.53.3x 00:01:E7:xx:xx:xx
ARP 193.144.53.1x 08:00:09:xx:xx:xx
ARP 193.144.53.1x 00:04:75:xx:xx:xx
ARP 193.144.53.x3 00:0A:5E:xx:xx:xx
ARP 193.144.53.x6 00:00:E8:xx:xx:xx
ARP 193.144.53.x4 00:0D:61:xx:xx:xx
ARP 193.144.53.x7 00:0F:B0:xx:xx:xx
ARP 193.144.53.x3 00:04:75:xx:xx:xx
ARP 193.144.53.2x 00:50:FC:xx:xx:xx
ARP 193.144.53.x1 00:50:FC:xx:xx:xx
ARP 193.144.53.x3 00:A0:D2:xx:xx:xx
END



Se ha perdido la conexión con el host.


 


Pues eso es todo por ahora. Espero que os anime a crear servicios de windows. Es realmente fácil desde VS.

3 comentarios sobre “InfoArp: Invocar al Api Win32, crear un servidor TCP y montar un servicio de windows con .Net”

  1. Hola:
    Interesante, practico, le felicito.
    Podria ayudarme que significa, cuenta limitada, esta bloquea hasta la entrada al escritorio de windows.
    que soluciones existen?.
    Y como se realizan?.
    Gracias de antemano

Deja un comentario

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *