diff --git a/documentation/RELEASE_NOTES.html b/documentation/RELEASE_NOTES.html index faf6e56ba..a80ae4ea8 100644 --- a/documentation/RELEASE_NOTES.html +++ b/documentation/RELEASE_NOTES.html @@ -30,6 +30,51 @@ release.

--> + +

Channel Access Security: Check Hostname Against DNS

+ +

Host names given in a HAG entry of an IOC's Access Security +Configuration File (ACF) have to date been compared against the hostname +provided by the CA client at connection time, which may or may not be the actual +name of that client. This allows rogue clients to pretend to be a different +host, and the IOC would believe them.

+ +

An option is now available to cause an IOC to ask its operating system to +look up the IP address of any hostnames listed in its ACF (which will normally +be done using the DNS or the /etc/hosts file). The IOC will then +compare the resulting IP address against the client's actual IP address when +checking access permissions at connection time. This name resolution is performed +at ACF file load time, which has a few consequences:

+ +
    + +
  1. If the DNS is slow when the names are resolved this will delay the process +of loading the ACF file.
  2. + +
  3. If a host name cannot be resolved the IOC will proceed, but this host name +will never be matched.
  4. + +
  5. Any changes in the hostname to IP address mapping will not be picked up by +the IOC unless and until the ACF file gets reloaded.
  6. + +
+ +

Optionally, IP addresses may be added instead of, or in addition to, host names +in the ACF file.

+ +

This feature can be enabled before iocInit with

+ +
+var("asCheckClientIP",1)
+
+ +

or with the VxWorks target shell use

+ +
+asCheckClientIP = 1
+
+ +

New and modified epicsThread APIs

epicsThreadCreateOpt()

diff --git a/modules/database/src/ioc/rsrv/camessage.c b/modules/database/src/ioc/rsrv/camessage.c index 72a4b17a1..f54bb4888 100644 --- a/modules/database/src/ioc/rsrv/camessage.c +++ b/modules/database/src/ioc/rsrv/camessage.c @@ -861,6 +861,14 @@ static int host_name_action ( caHdrLargeArray *mp, void *pPayload, return RSRV_ERROR; } + /* after all validation */ + if(asCheckClientIP) { + + DLOG (2, ( "CAS: host_name_action for \"%s\" ignores client provided host name\n", + client->pHostName ) ); + return RSRV_OK; + } + /* * user name will not change if there isnt enough memory */ diff --git a/modules/database/src/ioc/rsrv/caservertask.c b/modules/database/src/ioc/rsrv/caservertask.c index f377d837f..7a9ae63b3 100644 --- a/modules/database/src/ioc/rsrv/caservertask.c +++ b/modules/database/src/ioc/rsrv/caservertask.c @@ -1421,6 +1421,20 @@ struct client *create_tcp_client (SOCKET sock , const osiSockAddr *peerAddr) } client->addr = peerAddr->ia; + if(asCheckClientIP) { + epicsUInt32 ip = ntohl(client->addr.sin_addr.s_addr); + client->pHostName = malloc(24); + if(!client->pHostName) { + destroy_client ( client ); + return NULL; + } + epicsSnprintf(client->pHostName, 24, + "%u.%u.%u.%u", + (ip>>24)&0xff, + (ip>>16)&0xff, + (ip>>8)&0xff, + (ip>>0)&0xff); + } /* * see TCP(4P) this seems to make unsolicited single events much diff --git a/modules/database/src/ioc/rsrv/server.h b/modules/database/src/ioc/rsrv/server.h index 4d502f77f..6392c692b 100644 --- a/modules/database/src/ioc/rsrv/server.h +++ b/modules/database/src/ioc/rsrv/server.h @@ -86,7 +86,7 @@ typedef struct client { ELLLIST chanList; ELLLIST chanPendingUpdateARList; ELLLIST putNotifyQue; - struct sockaddr_in addr; + struct sockaddr_in addr; /* peer address, TCP only */ epicsTimeStamp time_at_last_send; epicsTimeStamp time_at_last_recv; void *evuser; diff --git a/modules/libcom/src/as/asLib.h b/modules/libcom/src/as/asLib.h index 261e5ed7d..528ce6ed9 100644 --- a/modules/libcom/src/as/asLib.h +++ b/modules/libcom/src/as/asLib.h @@ -21,6 +21,11 @@ extern "C" { #endif +/* 0 - Use (unverified) client provided host name string. + * 1 - Use actual client IP address. HAG() are resolved to IPs at ACF load time. + */ +epicsShareExtern int asCheckClientIP; + typedef struct asgMember *ASMEMBERPVT; typedef struct asgClient *ASCLIENTPVT; typedef int (*ASINPUTFUNCPTR)(char *buf,int max_size); @@ -165,8 +170,8 @@ typedef struct uag{ } UAG; /*Defs for Host Access Groups*/ typedef struct{ - ELLNODE node; - char *host; + ELLNODE node; + char host[1]; } HAGNAME; typedef struct hag{ ELLNODE node; diff --git a/modules/libcom/src/as/asLibRoutines.c b/modules/libcom/src/as/asLibRoutines.c index 3f5713efc..ab0bf5071 100644 --- a/modules/libcom/src/as/asLibRoutines.c +++ b/modules/libcom/src/as/asLibRoutines.c @@ -15,6 +15,8 @@ #include #define epicsExportSharedSymbols +#include "osiSock.h" +#include "epicsTypes.h" #include "epicsStdio.h" #include "dbDefs.h" #include "epicsThread.h" @@ -27,6 +29,8 @@ #include "postfix.h" #include "asLib.h" +int asCheckClientIP; + static epicsMutexId asLock; #define LOCK epicsMutexMustLock(asLock) #define UNLOCK epicsMutexUnlock(asLock) @@ -1203,14 +1207,38 @@ static HAG *asHagAdd(const char *hagName) static long asHagAddHost(HAG *phag,const char *host) { HAGNAME *phagname; - int len, i; if (!phag) return 0; - len = strlen(host); - phagname = asCalloc(1, sizeof(HAGNAME) + len + 1); - phagname->host = (char *)(phagname + 1); - for (i = 0; i < len; i++) { - phagname->host[i] = (char)tolower((int)host[i]); + if(!asCheckClientIP) { + size_t i, len = strlen(host); + phagname = asCalloc(1, sizeof(HAGNAME) + len); + for (i = 0; i < len; i++) { + phagname->host[i] = (char)tolower((int)host[i]); + } + + } else { + struct sockaddr_in addr; + epicsUInt32 ip; + + if(aToIPAddr(host, 0, &addr)) { + static const char unresolved[] = "unresolved:"; + + errlogPrintf("ACF: Unable to resolve host '%s'\n", host); + + phagname = asCalloc(1, sizeof(HAGNAME) + sizeof(unresolved)-1+strlen(host)); + strcpy(phagname->host, unresolved); + strcat(phagname->host, host); + + } else { + ip = ntohl(addr.sin_addr.s_addr); + phagname = asCalloc(1, sizeof(HAGNAME) + 24); + epicsSnprintf(phagname->host, 24, + "%u.%u.%u.%u", + (ip>>24)&0xff, + (ip>>16)&0xff, + (ip>>8)&0xff, + (ip>>0)&0xff); + } } ellAdd(&phag->list, &phagname->node); return 0; diff --git a/modules/libcom/src/iocsh/libComRegister.c b/modules/libcom/src/iocsh/libComRegister.c index b105ea12f..8fd5e7924 100644 --- a/modules/libcom/src/iocsh/libComRegister.c +++ b/modules/libcom/src/iocsh/libComRegister.c @@ -12,6 +12,7 @@ #define epicsExportSharedSymbols #include "iocsh.h" +#include "asLib.h" #include "epicsStdioRedirect.h" #include "epicsString.h" #include "epicsTime.h" @@ -393,6 +394,8 @@ static void installLastResortEventProviderCallFunc(const iocshArgBuf *args) installLastResortEventProvider(); } +static iocshVarDef asCheckClientIPDef = {"asCheckClientIP", iocshArgInt, 0}; + void epicsShareAPI libComRegister(void) { iocshRegister(&dateFuncDef, dateCallFunc); @@ -425,4 +428,7 @@ void epicsShareAPI libComRegister(void) iocshRegister(&generalTimeReportFuncDef,generalTimeReportCallFunc); iocshRegister(&installLastResortEventProviderFuncDef, installLastResortEventProviderCallFunc); + + asCheckClientIPDef.pval = &asCheckClientIP; + iocshRegisterVariable(&asCheckClientIPDef); } diff --git a/modules/libcom/test/aslibtest.c b/modules/libcom/test/aslibtest.c index 875aa56fd..4237fafb1 100644 --- a/modules/libcom/test/aslibtest.c +++ b/modules/libcom/test/aslibtest.c @@ -81,8 +81,8 @@ static const char hostname_config[] = "" static void testHostNames(void) { - testDiag("testHostNames()"); + asCheckClientIP = 0; testOk1(asInitMem(hostname_config, NULL)==0); @@ -102,18 +102,53 @@ static void testHostNames(void) testAccess("ro", 0); testAccess("rw", 0); - setHost("nosuchhost"); + setHost("guaranteed.invalid."); testAccess("invalid", 0); testAccess("DEFAULT", 0); testAccess("ro", 0); testAccess("rw", 0); } + +static void testUseIP(void) +{ + testDiag("testUseIP()"); + asCheckClientIP = 1; + + /* still host names in .acf */ + testOk1(asInitMem(hostname_config, NULL)==0); + /* now resolved to IPs */ + + setUser("testing"); + setHost("localhost"); /* will not match against resolved IP */ + asAsl = 0; + + testAccess("invalid", 0); + testAccess("DEFAULT", 0); + testAccess("ro", 0); + testAccess("rw", 0); + + setHost("127.0.0.1"); + + testAccess("invalid", 0); + testAccess("DEFAULT", 0); + testAccess("ro", 1); + testAccess("rw", 3); + + setHost("guaranteed.invalid."); + + testAccess("invalid", 0); + testAccess("DEFAULT", 0); + testAccess("ro", 0); + testAccess("rw", 0); +} + MAIN(aslibtest) { - testPlan(14); + testPlan(27); testSyntaxErrors(); testHostNames(); + testUseIP(); errlogFlush(); return testDone(); }