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:
+
+
+
+- If the DNS is slow when the names are resolved this will delay the process
+of loading the ACF file.
+
+- If a host name cannot be resolved the IOC will proceed, but this host name
+will never be matched.
+
+- 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.
+
+
+
+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();
}