6 dic 2017

Subiendo archivos a OneDrive desde IBM i / AS/400




Quieren subir archivos a OneDrive ?

Recuerden, si tienen Office 365 personal cuentan con 1 TB de datos mientras dure la suscripción. Es super simple: Tienen que ir primero a la siguiente URL y descargar en la PC el Java OneDrive Client del usuario "wootie": http://bit.ly/2Ah1wJiIBM i /AIX : Luego seguir las instrucciones de cómo compilar con Java 7, lo cual también es simple (deben instalar Java 7 SDK y Maven):  mvn clean package -DskipTests   Luego deben ejecutar lo siguiente: java -jar onedrive-java-client.jar -a  (cuidado, en la documentación no está correcto el comendo) Deben ir a la dirección que les aparece en el navegador, presionar SI en el botón que aparecerá, copiar la URL que quede en el navegador cuando la pantalla se ponga en blanco y pegar el contenido en el archivo "onedrive.key" dentro del mismo directorio. Con estos datos podemos transferir el archivo JAR y el archivo .key al IBM i o a AIX.  Para subir archivos simplemente haremos: java -jar onedrive-java-client.jar --direction UP --local /home/dkesselman/ARCHIVOS --remote IBMiArchivos/IBM i /AIX : * /home/dkesselman/ARCHIVOS es la carpeta a "sincronizar" * UP es el sentido de la sincronización (del IBMi hacia OneDrive) * IBMiArchivos  es la carpeta remota donde dejaremos los archivos enviados 

Si se complican en la compilación, con gusto puedo enviarles el archivo .JAR listo para usarse.

IBM i: Journal de auditoría - Buscando las migajas de pan



La mayoría de quienes trabajamos con IBM i sabemos de qué se trata el journal de auditoría. Lamentablemente no todos lo usan, muchos menos lo consultan.
Con el journal de auditoría podemos saber quien está intentando hacer algo indebido, podemos determinar la IP, desde que estación de trabajo se conectó, que comandos intentó usar, etc.
Si nos ponemos a definir las razones por las cuales mucho no lo explotan podríamos mencionar:
  • Espacio en disco: Es cierto, puede ocupar mucho espacio si no lo controlamos
  • Desconocimiento: Aunque muchos teman admitirlo, no todos lo conocen al 100%
  • Tiempo: Si, hay que invertir tiempo en revisar la información.
  • Complejidad y/o volumen de la información que se recupere
Que pasaría si podemos reducir estos problemas para sacarle el jugo a este recurso fantástico que incluye nuestro servidor?
Si podemos separar sólo la información que nos interesa y almacenarlo en tablas quizá podamos ir depurando los journals antiguos.
Si automatizamos la conversión y la generación de reportes podríamos reducir el tiempo que empleamos
Si pudiéramos tener una serie de templates o una forma simple de hacer consultas podríamos reducir la complejidad de información a leer.
Manos a la obra
Primero lo primero, debemos activarlo. Esto requiere de los siguientes pasos:
1) Crear una biblioteca para los receivers
CRTLIB LIB(AUDJRN) TEXT('Journal Receivers de auditoria') AUT(*EXCLUDE) CRTAUT(*EXCLUDE) 

2) Creo el 1er receiver 
CRTJRNRCV JRNRCV(AUDJRN/AUDJRN0000)  

3) Creo el journal de auditoría
CRTJRN JRN(QSYS/QAUDJRN) JRNRCV(AUDJRN/AUDJRN0000) JRNOBJLMT(*MAX10M)   

Ahora debemos configurar los valores del sistema para "encender" el journal e indicar que información queremos guardar
1) Activo el journal
CHGSYSVAL SYSVAL(QAUDCTL) VALUE('*AUDLVL *NOQTEMP *OBJAUD')

2) Valores a auditar (Según la necesidad, estos son algunos básicos)
CHGSYSVAL SYSVAL(QAUDLVL) VALUE('*AUDLVL2')
CHGSYSVAL SYSVAL(QAUDLVL2) VALUE('*AUTFAIL *CREATE  *DELETE  *JOBDTA  *NETCMN  *SAVRST  *SECURITY *SERVICE *SYSMGT  *PGMFAIL')

Y luego debemos indicar los comandos, objetos y usuarios a auditar:
1) Usuarios
CHGUSRAUD USRPRF(USRTST01) OBJAUD(*CHANGE) AUDLVL(*CMD *NETBAS *JOBDTA *CREATE *DELETE)

2) Objetos/comandos
CHGOBJAUD OBJ(QSYS/CHGJOB) OBJTYPE(*CMD) OBJAUD(*ALL)

A partir de ejecutar todos estos comandos estaremos recogiendo información, según los valores que hayamos indicado.
El punto está, cómo podemos ver la información ? Naturalmente usamos el comando DSPJRN, pero la información no se ve en una forma muy amigable, aunque sí es bastante veloz. Toda la información se muestra agolpada, lo cual dificulta su lectura.
Por otro lado, sólo podemos verlo desde pantalla verde, lo cual no siempre es lo ideal. Quienes tengan departamento de auditoría muchas veces deben lidiar con el desconocimiento de la plataforma y la separación de roles.
No sería mejor poder hacer consultas usando SQL sin mayor trámite? En este ejemplo listamos las entradas PW (fallas de password) de las últimas 48 horas en direcciones diferentes a 10.* y 192.* para determinar conexiones externas (claro, si ese es nuestro direccionamiento interno)
SELECT *                                              
FROM TABLE(QSYS2.Display_Journal('QSYS', 'QAUDJRN',   
STARTING_TIMESTAMP => CURRENT TIMESTAMP - 48 HOURS , 
STARTING_RECEIVER_NAME => '*CURCHAIN'                 
)) AS x                                               
where JOURNAL_entry_type = 'PW'                       
and REMOTE_ADDRESS not like '10.%'                    
and REMOTE_ADDRESS not like '192.%'                   

IBM nos provee de una serie de UDTFs que podemos usar para consultas de este tipo. Sin embargo el resultado sigue siendo genérico, no tendremos campos específicos.
Para poder ver la información en forma más amigable, IBM nos provee de plantillas que podemos copiar y usar como un "cascarón" donde copiamos la información, pero si queremos hacerlo de forma más simple, podemos usar el comando CPYAUDJRNE:
CPYAUDJRNE ENTTYP(AF PW CP SV DS VC VN VS X0)          
             OUTFILE(ESSELWARE/QAUDIT) OUTMBR(*FIRST 
             *ADD) JRNRCV(*CURCHAIN) FROMTIME(28112017 000000)                                    

Si creamos un par de tablas con las descripciones que aparecen en los manuales, podríamos hacer consultas que sean más legibles:
SELECT PWTSTP as Fecha,
PWUSRN as Usuario, 
PWJOB as Trabajo,
PWNBR as Nro_Trabajo, 
PWPGM as Programa,
PWPGMLIB as Biblioteca,
trim(PWRADR) as Direccion_IP, 
B.PWTYPEDS as Tipo_de_Intento 
from ESSELWARE.QAUDITPW a, 
ESSELWARE.AUDPWTYPE b  
where a.PWTYPE = b.PWTYPE 
and PWTSTP >= '2017-11-01' 
and PWTSTP < '2017-12-01' 
order by PWTSTP

Podemos crear un simple CL que corra el comando CPYAUDJRNE todas las madrugadas para descargar la información en tablas auxiliares de los valores que nos interesan:
PW: Fallas de autenticación
AF: Fallas de autorización (intentos fallidos de acceder a objetos)
SV: Cambios a valores del sistema
CD: Comandos
CP: Cambios, creación o restauración de perfiles de usuarios
DS: Reset de password del usuario de DST (recuerden que con el DST podemos recuperar el QSECOFR)
ST: Uso de Herramientas de Servicio (recuerden que con el DST podemos recuperar el QSECOFR)
VS: Iniciando o terminando una sesión de servidor
... y los valores que consideren necesarios.
Luego podríamos enviar mediante un ETL, DataPump o CSV->FTP los datos a una base de datos auxiliar (MySQL? SQLite? SQL Server?) para la explotación por parte los oficiales de seguridad.
En mi caso uso unos pequeños programas PHP para generar los reportes. Si les interesa el código con gusto se los comparto. Envíen comentarios o mensajes para solicitarlos.
Algunos consejos
  • Controlen el espacio en disco
  • Revisen las entradas sensibles de forma periódica. Incluso hay gente que cuenta con un programa de envío diario de reportes en PDF. Esto se podría hacer sin mucha complicación con un CL bastante simple.
  • Protejan la biblioteca de los receivers
  • Respalden los journals antes de borrarlos. La mejor práctica es hacerlo en una cinta separada, quizá junto con los journals de tablas (tema para otro artículo).
  • Algunas entradas tienen información parcial o de forma poco amigable, como podrían ser las conexiones ODBC/JDBC/OleDB/ADO. En esos casos quizá convenga generar logs en tablas usando Exit Point Programs.
  • Si existen requerimientos regulatorios siempre recomiendo usar herramientas, que ayudan a automatizar la generación de reportes y suelen tener plantillas para las auditorías. Algunas de estas herramientas incluso se llevan la información de forma continua a otra base de datos por aquello de la separación de roles.
  • Si el período a analizar es grande (3 o más meses) y el espacio a emplear es considerable, nuevamente, hay herramientas que administran de forma eficiente el espacio y suelen llevarse el análisis a un equipo separado. Recuerden que el costo x TB en IBM i suele ser más alto que 1 TB en la nube o en un servidor con Intel y MariaDB/MySQL/SQLite
Espero que les haya sido de utilidad.

IBM i - AS/400 : Capturando las instrucciones SQL ... (2) - Plan Cache Snapshots



Retomando el tema de las instrucciones ODBC, hoy vamos a verlas desde el punto de vista del rendimiento. Esto no significa que no podamos usarlo con fines de auditoria o forensia, pero no ha sido pensado con ese fin, aunque pueda resultar efectivo.
Normalmente cuando tenemos problemas con el rendimiento de la base de datos y no encontramos que está ocurriendo, lo primero que nos dice el especialista suele ser arrancar el DBMon o un supervisor de SQL.
Estos mecanismos son muy útiles para poder determinar problemas de rendimiento, errores en los queries, comportamientos anómalos, etc. Sin embargo tienen la gran desventaja de un volumen de información exageradamente alto, lo cual impide correrlos por períodos prolongados. Por prologado hablamos de más de 1 o 2 horas. A esto debemos sumar que en algunos casos generan carga y puede entorpecer la ejecución de procesos en su tiempo óptimo.
A nuestro rescate llega el IBM i ACS (el reemplazo Java del Cliente Access), que desde BASE DE DATOS-> SQL Performance Center-> Plan Cache Snapshots nos permite crear un volcado del plan cache en un archivo, pudiendo aplicar filtros.
Es importante tener el IBM Navigator for i funcionando. Deben recordar instalar los PTFs de HTTP, DB2 y Java, aparte de los acumulativos correspondientes. Si no lo tenemos funcionando también podremos tomar el snapshot usando Client Access o línea de desde línea de mandatos con STRSQL:
  CALL qsys2.dump_plan_cache('ESSELWARE','BMPRD171120');
El primer parámetro es la biblioteca, el 2do el nombre del snapshot.
Este volcado nos mostrará la mayoría de los comandos SQL que se hayan ejecutado desde el último IPL. Es importante destacar que no todos los comandos SQL son salvados en memoria, ya que algunos programas recalculan el plan, dando flush a mucha información valiosa.
Sin embargo, es un mecanismo veloz, consume pocos recursos, y podemos indicar que queremos las instrucciones que se están ejecutando o sólo la información histórica.
Con botón derecho sobre el snapshot nos permite ver diferentes opciones:
  • Analizar
  • Mostrar instrucciones SQL
  • Investigar
  • Comentarios
Nos centraremos en los 2 primeros puntos
Analizar
Nos permite ver información estadística de las instrucciones SQLs, desde el tipo de instrucción, hasta la duración o grado de proceso paralelo empleado, el tipo de denominación usada, etc.
Si selecciono de la lista junto a Sentencias SQL la opción de Resumen podré ver un resumido de las instrucciones que guardó en los archivos de estadísticas de forma tabular. Con Detalle aparecerán todas, incluso las repetidas.
Moviéndome hacia la derecha se pueden ver las instrucciones capturadas y datos estadísticos de cada uno, incluyendo ID del proceso.
Pero lo interesante es poder ver el Visual Explain, que es una de las gemas del iACS y del Client Access.
Parados sobre el registro que nos interesa damos click derecho y seleccionamos Trabajar con sentencias SQL. Luego, en la ventana de Run SQL Scripts podemos darle al botón que indica Explain. Algunas veces deberemos corregir el nombre de la biblioteca o reemplazar una "/" por un "."
Los resultados pueden ser más que interesantes, ya que nos puede advertir de falta de índices, operaciones de full-scan, búsquedas no optimizadas con simples filtrados, etc. y podemos descomponer la instrucción en diferentes tareas para entender al forma en que ejecuta.
Recuerdo haber envidiado a los DBAs de Oracle por una herramienta que permitía ver el plan de ejecución, pero con Visual Explain quedé satisfecho.
Lo importante es que podemos capturar toda esta información sin tener que hacer un trace, y el tiempo de tomar el snapshot, que obviamente tiene relación con la cantidad de instrucciones a guardar, es aceptable (por lo general no supera los 5 o 10 minutos).
Espero que les sea útil. Pueden dejarme comentarios si tienen dudas o consultas.

IBM i - AS/400 : Capturando las instrucciones SQL que están ejecutando en este momento - ODBC/JDBC



Cuando tenemos programas que se conectan por JDBC u ODBC y al mirar con WRKACTJOB en los trabajos QZDASOINIT nos comenzamos a rascar la cabeza tratando de adivinar que está ocurriendo.
Si nuestra aplicaciones son Cliente/Servidor y cada usuario se conecta con un perfil diferente al menos podemos distinguir a quien corresponde cada conexión con el CURRENT USER y podemos obtener su dirección IP revisando dentro del trabajo.
Cuando tenemos aplicaciones web, por lo general todos se conectan usando un mismo perfil de usuario y una misma dirección IP , lo cual se puede volver desconcertante. Los usuarios suelen estar declarados en tablas de la base de datos o en un directorio activo o LDAP, lo cual no es fácil de filtrar y detectar.
Ni hablemos de saber que está ocurriendo en ese momento en que queremos ver su SQL, porque en ninguno de los dos casos podremos verlo a menos que activemos el DBMON o un supervisor de SQL para luego analizarlo.
No sería fantástico poder ver las instrucciones SQL de la misma forma que usamos el WRKACTJOB?
Prueben esto:
WITH ACTIVE_USER_JOBS (Q_JOB_NAME,  CPU_TIME, RUN_PRIORITY) 
AS (SELECT JOB_NAME, CPU_TIME, RUN_PRIORITY 
FROM TABLE (QSYS2.ACTIVE_JOB_INFO('NO','','','')) x 
WHERE JOB_TYPE <> 'SYS'SELECT Q_JOB_NAME, 
CPU_TIME, 
RUN_PRIORITY, 
CURRENT TIMESTAMP - V_SQL_STMT_START_TIMESTAMP AS SQL_STMT_DURATION, 
B.* 
FROM ACTIVE_USER_JOBS, TABLE(QSYS2.GET_JOB_INFO(Q_JOB_NAME)) B 
WHERE V_SQL_STMT_STATUS = 'ACTIVE'
ORDER BY SQL_STMT_DURATION DESC
Esto les mostrará los comandos SQL de los trabajos activos.
En pocas palabras, estamos usando una serie de UDTFs nuevos que corresponden a V7R2 y que proveen servicios similares al WRKACTJOB, pero con mayor nivel de detalle.
Ahora bien, entre todos los valores que podemos recuperar hay unos registros especiales y EDITABLES!!! Esto significa que de forma programática podríamos guardar valores desde nuestro programa como si fueran variables y consultarlos.
V_CLIENT_WRKSTNNAME CHAR(255) Value of the SQL CLIENT_WRKSTNNAME special register.
V_CLIENT_APPLNAME CHAR(255) Value of the SQL CLIENT_APPLNAME special register.
V_CLIENT_ACCTNG     CHAR(255) Value of the SQL CLIENT_ACCTNG special register.
V_CLIENT_PROGRAMID CHAR(255) Value of the SQL CLIENT_PROGRAMID special register.
V_CLIENT_USERID     CHAR(255) Value of the SQL CLIENT_USERID special register.

Qué podríamos guardar?
IP del usuario final, Nombre de la aplicación, Nombre del programa que ejecuta, Usuario local al sistema y cualquier otro valor que nos permita identificar la sesion.
Para poder actualizar esos valores deberemos comandos como este, o jugar con las propiedades de la conexión. Me quedo con el stored procedure:
  CALL SYSPROC.WLM_SET_CLIENT_INFO('MyUser', 'https://www.linkedin.com/redir/invalid-link-page?url=192%2e168%2e123%2e123', 
     'CORE Banking', 'Modulo1', 'ProgramaXYZ02')

Y eso permitirá combinar la información de SQL, el programa asociado, la IP del cliente y su nombre de usuario.
Es un poco lo que usamos con WEBACTJOB, aunque con algunos agregados para poder terminar el trabajo, ver su joblog ,etc.
En un próximo artículo les explicaré como podemos tomar una "foto" de todos los trabajos corriendo con sus conexiones y comandos SQL para poder analizar que ocurre, sin tener que ejecutar el DBMON.
Suerte

Controlando las conexiones por ODBC/JDBC en IBM i




Aún cuando los trabajos QZDASOINIT existen desde hace muchos años permitiéndonos conectar mediante ODBC o JDBC ha sido en los últimos años, desde V6R1 , que hemos visto un incremento de este tipo de conexiones, particularmente de servidores y procesos de extracción de datos.
Y a medida que tenemos más conexiones también se incrementa la complejidad de las conexiones y la dificultad para controlar quien se conecta, en que horario, con qué mecanismo, etc.
Mucha de esta información se puede obtener empleando el journal de auditoría del sistema, un producto muy conocido y no tan usado. Sin embargo la información que provee el journal puede ser algo confusa y no siempre brinda todo el contenido que necesitamos y por otro lado no nos permite tomar acción, como podría ser evitando esa conexión si no cumple con ciertas reglas definidas por nosotros.
IBM nos brinda EXIT POINTS para controlar las comunicaciones a la base de datos (y muchas cosas más), los cuales podemos usar para monitorear o filtrar estas conexiones mediante un EXIT POINT PROGRAM.
Existen varios de estos EXIT POINTS para controlar las conexiones a la base de datos, pero sólo nos enfocaremos en las conexiones remotas mediante QIBM_QZDA_INIT .
Se pueden controlar también los comandos SQL y otros tipos de conexión, pero si no somos cuidadosos podríamos hacer más lentas nuestras aplicaciones. Mi recomendación es que usen productos comerciales si tienen necesidades de controlar las sentencias SQL o ejecución de Stored Procedures. Hay productos como Data Thread de HelpSystems que hacen su tarea de forma muy eficiente y simple.
Este es un ejemplo de cómo guardar información de la conexión. Adaptar este CL para filtrar por valores de IP, Usuario, Horario, etc lo dejo a criterio del lector, ya que no es tan complejo partiendo de este ejemplo.
Este CL se basa en el publicado por Michael Swenson en IBM System Magazine
PGM PARM(&STATUS &REQUEST)

/********************************************************************/
/*  Declaracion de parametros                                 */
/********************************************************************/

DCL VAR(&STATUS)  TYPE(*CHAR) LEN(1)    /* ACCEPT OR REJECT         */
DCL VAR(&REQUEST) TYPE(*CHAR) LEN(2000) /* PARAMETER STRUCTURE      */


/********************************************************************/
/*  Declaracion de variables                                        */
/********************************************************************/

DCL VAR(&USER)    TYPE(*CHAR) LEN(10)    /* USER ID                */
DCL VAR(&ITYPE)   TYPE(*CHAR) LEN(63)    /* Interface type         */
DCL VAR(&INAME)   TYPE(*CHAR) LEN(127)   /* Interface name         */
DCL VAR(&ILEVEL)  TYPE(*CHAR) LEN(63)    /* Interface level        */
DCL VAR(&STMT)    TYPE(*CHAR) LEN(512)   /* Insert statement       */
DCL VAR(&CMD)     TYPE(*CHAR) LEN(512)   /* Comando QSHELL         */

DCL VAR(&RCVVAR) TYPE(*CHAR) LEN(332)
DCL VAR(&RCVLEN) TYPE(*CHAR) LEN(4)
DCL VAR(&FMTNAM) TYPE(*CHAR) LEN(8) VALUE('JOBI0600')
DCL VAR(&JOBNAM) TYPE(*CHAR) LEN(26) VALUE('*')
DCL VAR(&INTJOB) TYPE(*CHAR) LEN(16)
DCL VAR(&JOBNUM) TYPE(*CHAR) LEN(8)

DCL        VAR(&JOB_IP) TYPE(*CHAR) LEN(15)
DCL        VAR(&JOB_NAME) TYPE(*CHAR) LEN(10)
DCL        VAR(&JOB_NBR) TYPE(*CHAR) LEN(6)
DCL        VAR(&JOB_ID) TYPE(*CHAR) LEN(30)
DCL        VAR(&JOB_USER) TYPE(*CHAR) LEN(10)

DCL VAR(&Q)       TYPE(*CHAR) LEN(1)  VALUE('''')

 /********************************************************************/
 /*  Extraigo las variables del parametro                            */
 /********************************************************************/

   CHGVAR VAR(&USER)   VALUE(%SST(&REQUEST   1  10))
   CHGVAR VAR(&ITYPE)  VALUE(%SST(&REQUEST  33  63))
   CHGVAR VAR(&INAME)  VALUE(%SST(&REQUEST  96 127))
   CHGVAR VAR(&ILEVEL) VALUE(%SST(&REQUEST 223  63))

   CHGVAR VAR(&STATUS) VALUE('1')   /* Aceptar conexion */

   CHGVAR VAR(%SST(&RCVVAR 129 1)) VALUE('X')
   CHGVAR VAR(%BIN(&RCVLEN)) VALUE(332)
   /******************************************************************/
   /*  Recupero Job & IP                                             */
   /******************************************************************/
   RTVJOBA    JOB(&JOB_NAME) USER(&JOB_USER) NBR(&JOB_NBR)
   CHGVAR     VAR(&JOB_ID) VALUE(&JOB_NBR *TCAT '/' +
                  *TCAT &JOB_USER *TCAT '/' *TCAT &JOB_NAME)
   CALL       PGM(QSYS/QUSRJOBI) +
                  PARM(&RCVVAR &RCVLEN &FMTNAM &JOBNAM &INTJOB)
   CHGVAR VAR(&JOB_IP) VALUE(%SST(&RCVVAR 308 15))
   /******************************************************************/
     
     
   CHGVAR VAR(&STMT) VALUE(' INSERT INTO DBLOG.DBINITDATA ' *CAT +
       'Values (CURRENT_TIMESTAMP, ' *CAT &Q *CAT &USER *TCAT &Q +
       *CAT ', ' *CAT &Q *CAT &ITYPE *TCAT &Q *CAT ', ' *CAT &Q +
       *CAT &INAME *TCAT &Q *CAT ', ' *CAT &Q *CAT &ILEVEL +
       *TCAT &Q *CAT ', ' *CAT  &Q *TCAT &JOB_ID *TCAT &Q +
       *CAT ', ' *CAT &Q *TCAT &JOB_IP *TCAT &Q *CAT ' )  ')
      RUNSQL     SQL(&STMT) COMMIT(*NONE) NAMING(*SQL)
      MONMSG MSGID(CPF0000)                              
ENDPGM

Luego creamos la biblioteca DBLOG y el archivo DBINITDATA con la siguiente estructura:
ConnectionTime timestamp,
User           CHAR(10), 
InterfaceType  CHAR(63), 
InterfaceName  CHAR(127),
InterfaceLevel CHAR(63), 
JOB_ID         CHAR(30), 
JOB_IP         CHAR(30)) 
y asignamos permisos :
GRTOBJAUT OBJ(DBLOG/DBINITDATA) OBJTYPE(*FILE) USER(QUSER)
Ahora conectamos el EXIT POINT :
ADDEXITPGM EXITPNT(QIBM_QZDA_INIT) FORMAT(ZDAI0100) PGMNBR(1) PGM(ESSELWARE/ZDAINIT) TEXT('Registro conexiones ODBC/JDBC')                
Finalmente debemos bajar el HOST Server *DATABASE y los QZDASOINIT. Sin embargo, lo ideal es bajar TODOS los servicios o dar un IPL.
Manipulando la variable &STATUS podemos permitir la conexión o rechazarla (1 o 0).
Vuelvo a insistir, se pueden hacer programas para controlar las instrucciones SQL sin mucha complicación, pero la latencia que podría agregar el programa podría afectar negativamente el rendimiento de la base de datos. Recomiendo que consideren productos comerciales en esos casos.
Mucha suerte

Cómo conectarme a un servidor remoto en una red protegida - Versión actualizada

En un artículo anterior describí cómo conectarse a un equipo remoto en una red protegida http://diego-k.blogspot.mx/2014/12/como-conectarme...