960 lines
26 KiB
C
960 lines
26 KiB
C
/* $OpenBSD: ip_spd.c,v 1.120 2024/04/17 20:48:51 bluhm Exp $ */
|
|
/*
|
|
* The author of this code is Angelos D. Keromytis (angelos@cis.upenn.edu)
|
|
*
|
|
* Copyright (c) 2000-2001 Angelos D. Keromytis.
|
|
*
|
|
* Permission to use, copy, and modify this software with or without fee
|
|
* is hereby granted, provided that this entire notice is included in
|
|
* all copies of any software which is or includes a copy or
|
|
* modification of this software.
|
|
* You may use this code under the GNU public license if you so wish. Please
|
|
* contribute changes back to the authors under this freer than GPL license
|
|
* so that we may further the use of strong encryption without limitations to
|
|
* all.
|
|
*
|
|
* THIS SOFTWARE IS BEING PROVIDED "AS IS", WITHOUT ANY EXPRESS OR
|
|
* IMPLIED WARRANTY. IN PARTICULAR, NONE OF THE AUTHORS MAKES ANY
|
|
* REPRESENTATION OR WARRANTY OF ANY KIND CONCERNING THE
|
|
* MERCHANTABILITY OF THIS SOFTWARE OR ITS FITNESS FOR ANY PARTICULAR
|
|
* PURPOSE.
|
|
*/
|
|
|
|
#include <sys/param.h>
|
|
#include <sys/systm.h>
|
|
#include <sys/mbuf.h>
|
|
#include <sys/socket.h>
|
|
#include <sys/kernel.h>
|
|
#include <sys/socketvar.h>
|
|
#include <sys/pool.h>
|
|
#include <sys/timeout.h>
|
|
|
|
#include <net/route.h>
|
|
#include <net/netisr.h>
|
|
|
|
#include <netinet/in.h>
|
|
#include <netinet/ip.h>
|
|
#include <netinet/ip_var.h>
|
|
#include <netinet/in_pcb.h>
|
|
#include <netinet/ip_ipsp.h>
|
|
#include <net/pfkeyv2.h>
|
|
|
|
int ipsp_spd_inp(struct mbuf *, const struct ipsec_level *,
|
|
struct ipsec_policy *, struct tdb **);
|
|
int ipsp_acquire_sa(struct ipsec_policy *, union sockaddr_union *,
|
|
union sockaddr_union *, struct sockaddr_encap *, struct mbuf *);
|
|
int ipsp_pending_acquire(struct ipsec_policy *, union sockaddr_union *);
|
|
void ipsp_delete_acquire_timer(void *);
|
|
void ipsp_delete_acquire_locked(struct ipsec_acquire *);
|
|
void ipsp_delete_acquire(struct ipsec_acquire *);
|
|
void ipsp_unref_acquire_locked(struct ipsec_acquire *);
|
|
|
|
struct pool ipsec_policy_pool;
|
|
struct pool ipsec_acquire_pool;
|
|
|
|
/*
|
|
* For tdb_walk() calling tdb_delete_locked() we need lock order
|
|
* tdb_sadb_mtx before ipo_tdb_mtx.
|
|
*/
|
|
struct mutex ipo_tdb_mtx = MUTEX_INITIALIZER(IPL_SOFTNET);
|
|
|
|
/* Protected by the NET_LOCK(). */
|
|
struct radix_node_head **spd_tables;
|
|
unsigned int spd_table_max;
|
|
|
|
struct mutex ipsec_acquire_mtx = MUTEX_INITIALIZER(IPL_SOFTNET);
|
|
struct ipsec_acquire_head ipsec_acquire_head =
|
|
TAILQ_HEAD_INITIALIZER(ipsec_acquire_head);
|
|
|
|
struct radix_node_head *
|
|
spd_table_get(unsigned int rtableid)
|
|
{
|
|
unsigned int rdomain;
|
|
|
|
NET_ASSERT_LOCKED();
|
|
|
|
if (spd_tables == NULL)
|
|
return (NULL);
|
|
|
|
rdomain = rtable_l2(rtableid);
|
|
if (rdomain > spd_table_max)
|
|
return (NULL);
|
|
|
|
return (spd_tables[rdomain]);
|
|
}
|
|
|
|
struct radix_node_head *
|
|
spd_table_add(unsigned int rtableid)
|
|
{
|
|
struct radix_node_head *rnh = NULL;
|
|
unsigned int rdomain;
|
|
void *p;
|
|
|
|
NET_ASSERT_LOCKED_EXCLUSIVE();
|
|
|
|
rdomain = rtable_l2(rtableid);
|
|
if (spd_tables == NULL || rdomain > spd_table_max) {
|
|
if ((p = mallocarray(rdomain + 1, sizeof(*rnh),
|
|
M_RTABLE, M_NOWAIT|M_ZERO)) == NULL)
|
|
return (NULL);
|
|
|
|
if (spd_tables != NULL) {
|
|
memcpy(p, spd_tables, sizeof(*rnh) * (spd_table_max+1));
|
|
free(spd_tables, M_RTABLE,
|
|
sizeof(*rnh) * (spd_table_max+1));
|
|
}
|
|
spd_tables = p;
|
|
spd_table_max = rdomain;
|
|
}
|
|
|
|
if (spd_tables[rdomain] == NULL) {
|
|
if (rn_inithead((void **)&rnh,
|
|
offsetof(struct sockaddr_encap, sen_type)) == 0)
|
|
rnh = NULL;
|
|
spd_tables[rdomain] = rnh;
|
|
}
|
|
|
|
return (spd_tables[rdomain]);
|
|
}
|
|
|
|
int
|
|
spd_table_walk(unsigned int rtableid,
|
|
int (*func)(struct ipsec_policy *, void *, unsigned int), void *arg)
|
|
{
|
|
struct radix_node_head *rnh;
|
|
int (*walker)(struct radix_node *, void *, u_int) = (void *)func;
|
|
int error;
|
|
|
|
rnh = spd_table_get(rtableid);
|
|
if (rnh == NULL)
|
|
return (0);
|
|
|
|
/* EGAIN means the tree changed. */
|
|
while ((error = rn_walktree(rnh, walker, arg)) == EAGAIN)
|
|
continue;
|
|
|
|
return (error);
|
|
}
|
|
|
|
/*
|
|
* Lookup at the SPD based on the headers contained on the mbuf. The second
|
|
* argument indicates what protocol family the header at the beginning of
|
|
* the mbuf is. hlen is the offset of the transport protocol header
|
|
* in the mbuf.
|
|
*
|
|
* Return combinations (of return value and *tdbout):
|
|
* - -EINVAL -> silently drop the packet
|
|
* - errno -> drop packet and return error
|
|
* - 0/NULL -> no IPsec required on packet
|
|
* - 0/TDB -> do IPsec
|
|
*
|
|
* In the case of incoming flows, only the first three combinations are
|
|
* returned.
|
|
*/
|
|
int
|
|
ipsp_spd_lookup(struct mbuf *m, int af, int hlen, int direction,
|
|
struct tdb *tdbin, const struct ipsec_level *seclevel, struct tdb **tdbout,
|
|
struct ipsec_ids *ipsecflowinfo_ids)
|
|
{
|
|
struct radix_node_head *rnh;
|
|
struct radix_node *rn;
|
|
union sockaddr_union sdst, ssrc;
|
|
struct sockaddr_encap *ddst, dst;
|
|
struct ipsec_policy *ipo;
|
|
struct ipsec_ids *ids = NULL;
|
|
int error, signore = 0, dignore = 0;
|
|
u_int rdomain;
|
|
|
|
NET_ASSERT_LOCKED();
|
|
|
|
/*
|
|
* If there are no flows in place, there's no point
|
|
* continuing with the SPD lookup.
|
|
*/
|
|
if (!ipsec_in_use)
|
|
return ipsp_spd_inp(m, seclevel, NULL, tdbout);
|
|
|
|
/*
|
|
* If an input packet is destined to a BYPASS socket, just accept it.
|
|
*/
|
|
if ((seclevel != NULL) && (direction == IPSP_DIRECTION_IN) &&
|
|
(seclevel->sl_esp_trans == IPSEC_LEVEL_BYPASS) &&
|
|
(seclevel->sl_esp_network == IPSEC_LEVEL_BYPASS) &&
|
|
(seclevel->sl_auth == IPSEC_LEVEL_BYPASS)) {
|
|
if (tdbout != NULL)
|
|
*tdbout = NULL;
|
|
return 0;
|
|
}
|
|
|
|
memset(&dst, 0, sizeof(dst));
|
|
memset(&sdst, 0, sizeof(union sockaddr_union));
|
|
memset(&ssrc, 0, sizeof(union sockaddr_union));
|
|
ddst = (struct sockaddr_encap *)&dst;
|
|
ddst->sen_family = PF_KEY;
|
|
ddst->sen_len = SENT_LEN;
|
|
|
|
switch (af) {
|
|
case AF_INET:
|
|
if (hlen < sizeof (struct ip) || m->m_pkthdr.len < hlen)
|
|
return EINVAL;
|
|
|
|
ddst->sen_direction = direction;
|
|
ddst->sen_type = SENT_IP4;
|
|
|
|
m_copydata(m, offsetof(struct ip, ip_src),
|
|
sizeof(struct in_addr), (caddr_t) &(ddst->sen_ip_src));
|
|
m_copydata(m, offsetof(struct ip, ip_dst),
|
|
sizeof(struct in_addr), (caddr_t) &(ddst->sen_ip_dst));
|
|
m_copydata(m, offsetof(struct ip, ip_p), sizeof(u_int8_t),
|
|
(caddr_t) &(ddst->sen_proto));
|
|
|
|
sdst.sin.sin_family = ssrc.sin.sin_family = AF_INET;
|
|
sdst.sin.sin_len = ssrc.sin.sin_len =
|
|
sizeof(struct sockaddr_in);
|
|
ssrc.sin.sin_addr = ddst->sen_ip_src;
|
|
sdst.sin.sin_addr = ddst->sen_ip_dst;
|
|
|
|
/*
|
|
* If TCP/UDP, extract the port numbers to use in the lookup.
|
|
*/
|
|
switch (ddst->sen_proto) {
|
|
case IPPROTO_UDP:
|
|
case IPPROTO_TCP:
|
|
/* Make sure there's enough data in the packet. */
|
|
if (m->m_pkthdr.len < hlen + 2 * sizeof(u_int16_t))
|
|
return EINVAL;
|
|
|
|
/*
|
|
* Luckily, the offset of the src/dst ports in
|
|
* both the UDP and TCP headers is the same (first
|
|
* two 16-bit values in the respective headers),
|
|
* so we can just copy them.
|
|
*/
|
|
m_copydata(m, hlen, sizeof(u_int16_t),
|
|
(caddr_t) &(ddst->sen_sport));
|
|
m_copydata(m, hlen + sizeof(u_int16_t),
|
|
sizeof(u_int16_t),
|
|
(caddr_t) &(ddst->sen_dport));
|
|
break;
|
|
|
|
default:
|
|
ddst->sen_sport = 0;
|
|
ddst->sen_dport = 0;
|
|
}
|
|
|
|
break;
|
|
|
|
#ifdef INET6
|
|
case AF_INET6:
|
|
if (hlen < sizeof (struct ip6_hdr) || m->m_pkthdr.len < hlen)
|
|
return EINVAL;
|
|
|
|
ddst->sen_type = SENT_IP6;
|
|
ddst->sen_ip6_direction = direction;
|
|
|
|
m_copydata(m, offsetof(struct ip6_hdr, ip6_src),
|
|
sizeof(struct in6_addr),
|
|
(caddr_t) &(ddst->sen_ip6_src));
|
|
m_copydata(m, offsetof(struct ip6_hdr, ip6_dst),
|
|
sizeof(struct in6_addr),
|
|
(caddr_t) &(ddst->sen_ip6_dst));
|
|
m_copydata(m, offsetof(struct ip6_hdr, ip6_nxt),
|
|
sizeof(u_int8_t),
|
|
(caddr_t) &(ddst->sen_ip6_proto));
|
|
|
|
sdst.sin6.sin6_family = ssrc.sin6.sin6_family = AF_INET6;
|
|
sdst.sin6.sin6_len = ssrc.sin6.sin6_len =
|
|
sizeof(struct sockaddr_in6);
|
|
in6_recoverscope(&ssrc.sin6, &ddst->sen_ip6_src);
|
|
in6_recoverscope(&sdst.sin6, &ddst->sen_ip6_dst);
|
|
|
|
/*
|
|
* If TCP/UDP, extract the port numbers to use in the lookup.
|
|
*/
|
|
switch (ddst->sen_ip6_proto) {
|
|
case IPPROTO_UDP:
|
|
case IPPROTO_TCP:
|
|
/* Make sure there's enough data in the packet. */
|
|
if (m->m_pkthdr.len < hlen + 2 * sizeof(u_int16_t))
|
|
return EINVAL;
|
|
|
|
/*
|
|
* Luckily, the offset of the src/dst ports in
|
|
* both the UDP and TCP headers is the same
|
|
* (first two 16-bit values in the respective
|
|
* headers), so we can just copy them.
|
|
*/
|
|
m_copydata(m, hlen, sizeof(u_int16_t),
|
|
(caddr_t) &(ddst->sen_ip6_sport));
|
|
m_copydata(m, hlen + sizeof(u_int16_t),
|
|
sizeof(u_int16_t),
|
|
(caddr_t) &(ddst->sen_ip6_dport));
|
|
break;
|
|
|
|
default:
|
|
ddst->sen_ip6_sport = 0;
|
|
ddst->sen_ip6_dport = 0;
|
|
}
|
|
|
|
break;
|
|
#endif /* INET6 */
|
|
|
|
default:
|
|
return EAFNOSUPPORT;
|
|
}
|
|
|
|
/* Actual SPD lookup. */
|
|
rdomain = rtable_l2(m->m_pkthdr.ph_rtableid);
|
|
if ((rnh = spd_table_get(rdomain)) == NULL ||
|
|
(rn = rn_match((caddr_t)&dst, rnh)) == NULL) {
|
|
/*
|
|
* Return whatever the socket requirements are, there are no
|
|
* system-wide policies.
|
|
*/
|
|
return ipsp_spd_inp(m, seclevel, NULL, tdbout);
|
|
}
|
|
ipo = (struct ipsec_policy *)rn;
|
|
|
|
switch (ipo->ipo_type) {
|
|
case IPSP_PERMIT:
|
|
return ipsp_spd_inp(m, seclevel, ipo, tdbout);
|
|
|
|
case IPSP_DENY:
|
|
return EHOSTUNREACH;
|
|
|
|
case IPSP_IPSEC_USE:
|
|
case IPSP_IPSEC_ACQUIRE:
|
|
case IPSP_IPSEC_REQUIRE:
|
|
case IPSP_IPSEC_DONTACQ:
|
|
/* Nothing more needed here. */
|
|
break;
|
|
|
|
default:
|
|
return EINVAL;
|
|
}
|
|
|
|
/* Check for non-specific destination in the policy. */
|
|
switch (ipo->ipo_dst.sa.sa_family) {
|
|
case AF_INET:
|
|
if ((ipo->ipo_dst.sin.sin_addr.s_addr == INADDR_ANY) ||
|
|
(ipo->ipo_dst.sin.sin_addr.s_addr == INADDR_BROADCAST))
|
|
dignore = 1;
|
|
break;
|
|
|
|
#ifdef INET6
|
|
case AF_INET6:
|
|
if ((IN6_IS_ADDR_UNSPECIFIED(&ipo->ipo_dst.sin6.sin6_addr)) ||
|
|
(memcmp(&ipo->ipo_dst.sin6.sin6_addr, &in6mask128,
|
|
sizeof(in6mask128)) == 0))
|
|
dignore = 1;
|
|
break;
|
|
#endif /* INET6 */
|
|
}
|
|
|
|
/* Likewise for source. */
|
|
switch (ipo->ipo_src.sa.sa_family) {
|
|
case AF_INET:
|
|
if (ipo->ipo_src.sin.sin_addr.s_addr == INADDR_ANY)
|
|
signore = 1;
|
|
break;
|
|
|
|
#ifdef INET6
|
|
case AF_INET6:
|
|
if (IN6_IS_ADDR_UNSPECIFIED(&ipo->ipo_src.sin6.sin6_addr))
|
|
signore = 1;
|
|
break;
|
|
#endif /* INET6 */
|
|
}
|
|
|
|
/* Do we have a cached entry ? If so, check if it's still valid. */
|
|
mtx_enter(&ipo_tdb_mtx);
|
|
if (ipo->ipo_tdb != NULL &&
|
|
(ipo->ipo_tdb->tdb_flags & TDBF_INVALID)) {
|
|
TAILQ_REMOVE(&ipo->ipo_tdb->tdb_policy_head, ipo,
|
|
ipo_tdb_next);
|
|
tdb_unref(ipo->ipo_tdb);
|
|
ipo->ipo_tdb = NULL;
|
|
}
|
|
mtx_leave(&ipo_tdb_mtx);
|
|
|
|
/* Outgoing packet policy check. */
|
|
if (direction == IPSP_DIRECTION_OUT) {
|
|
/*
|
|
* If the packet is destined for the policy-specified
|
|
* gateway/endhost, and the socket has the BYPASS
|
|
* option set, skip IPsec processing.
|
|
*/
|
|
if ((seclevel != NULL) &&
|
|
(seclevel->sl_esp_trans == IPSEC_LEVEL_BYPASS) &&
|
|
(seclevel->sl_esp_network == IPSEC_LEVEL_BYPASS) &&
|
|
(seclevel->sl_auth == IPSEC_LEVEL_BYPASS)) {
|
|
/* Direct match. */
|
|
if (dignore ||
|
|
!memcmp(&sdst, &ipo->ipo_dst, sdst.sa.sa_len)) {
|
|
if (tdbout != NULL)
|
|
*tdbout = NULL;
|
|
return 0;
|
|
}
|
|
}
|
|
|
|
/* Check that the cached TDB (if present), is appropriate. */
|
|
mtx_enter(&ipo_tdb_mtx);
|
|
if (ipo->ipo_tdb != NULL) {
|
|
if ((ipo->ipo_last_searched <= ipsec_last_added) ||
|
|
(ipo->ipo_sproto != ipo->ipo_tdb->tdb_sproto) ||
|
|
memcmp(dignore ? &sdst : &ipo->ipo_dst,
|
|
&ipo->ipo_tdb->tdb_dst,
|
|
ipo->ipo_tdb->tdb_dst.sa.sa_len))
|
|
goto nomatchout;
|
|
|
|
if (!ipsp_aux_match(ipo->ipo_tdb,
|
|
ipsecflowinfo_ids? ipsecflowinfo_ids: ipo->ipo_ids,
|
|
&ipo->ipo_addr, &ipo->ipo_mask))
|
|
goto nomatchout;
|
|
|
|
/* Cached entry is good. */
|
|
error = ipsp_spd_inp(m, seclevel, ipo, tdbout);
|
|
mtx_leave(&ipo_tdb_mtx);
|
|
return error;
|
|
|
|
nomatchout:
|
|
/* Cached TDB was not good. */
|
|
TAILQ_REMOVE(&ipo->ipo_tdb->tdb_policy_head, ipo,
|
|
ipo_tdb_next);
|
|
tdb_unref(ipo->ipo_tdb);
|
|
ipo->ipo_tdb = NULL;
|
|
ipo->ipo_last_searched = 0;
|
|
}
|
|
|
|
/*
|
|
* If no SA has been added since the last time we did a
|
|
* lookup, there's no point searching for one. However, if the
|
|
* destination gateway is left unspecified (or is all-1's),
|
|
* always lookup since this is a generic-match rule
|
|
* (otherwise, we can have situations where SAs to some
|
|
* destinations exist but are not used, possibly leading to an
|
|
* explosion in the number of acquired SAs).
|
|
*/
|
|
if (ipo->ipo_last_searched <= ipsec_last_added) {
|
|
struct tdb *tdbp_new;
|
|
|
|
/* "Touch" the entry. */
|
|
if (dignore == 0)
|
|
ipo->ipo_last_searched = getuptime();
|
|
|
|
/* gettdb() takes tdb_sadb_mtx, preserve lock order */
|
|
mtx_leave(&ipo_tdb_mtx);
|
|
/* Find an appropriate SA from the existing ones. */
|
|
tdbp_new = gettdbbydst(rdomain,
|
|
dignore ? &sdst : &ipo->ipo_dst,
|
|
ipo->ipo_sproto,
|
|
ipsecflowinfo_ids? ipsecflowinfo_ids: ipo->ipo_ids,
|
|
&ipo->ipo_addr, &ipo->ipo_mask);
|
|
ids = NULL;
|
|
mtx_enter(&ipo_tdb_mtx);
|
|
if ((tdbp_new != NULL) &&
|
|
(tdbp_new->tdb_flags & TDBF_DELETED)) {
|
|
/*
|
|
* After tdb_delete() has released ipo_tdb_mtx
|
|
* in tdb_unlink(), never add a new one.
|
|
* tdb_cleanspd() has to catch all of them.
|
|
*/
|
|
tdb_unref(tdbp_new);
|
|
tdbp_new = NULL;
|
|
}
|
|
if (ipo->ipo_tdb != NULL) {
|
|
/* Remove cached TDB from parallel thread. */
|
|
TAILQ_REMOVE(&ipo->ipo_tdb->tdb_policy_head,
|
|
ipo, ipo_tdb_next);
|
|
tdb_unref(ipo->ipo_tdb);
|
|
}
|
|
ipo->ipo_tdb = tdbp_new;
|
|
if (ipo->ipo_tdb != NULL) {
|
|
/* gettdbbydst() has already refcounted tdb */
|
|
TAILQ_INSERT_TAIL(
|
|
&ipo->ipo_tdb->tdb_policy_head,
|
|
ipo, ipo_tdb_next);
|
|
error = ipsp_spd_inp(m, seclevel, ipo, tdbout);
|
|
mtx_leave(&ipo_tdb_mtx);
|
|
return error;
|
|
}
|
|
}
|
|
mtx_leave(&ipo_tdb_mtx);
|
|
|
|
/* So, we don't have an SA -- just a policy. */
|
|
switch (ipo->ipo_type) {
|
|
case IPSP_IPSEC_REQUIRE:
|
|
/* Acquire SA through key management. */
|
|
if (ipsp_acquire_sa(ipo,
|
|
dignore ? &sdst : &ipo->ipo_dst,
|
|
signore ? NULL : &ipo->ipo_src, ddst, m) != 0) {
|
|
return EACCES;
|
|
}
|
|
|
|
/* FALLTHROUGH */
|
|
case IPSP_IPSEC_DONTACQ:
|
|
return -EINVAL; /* Silently drop packet. */
|
|
|
|
case IPSP_IPSEC_ACQUIRE:
|
|
/* Acquire SA through key management. */
|
|
ipsp_acquire_sa(ipo, dignore ? &sdst : &ipo->ipo_dst,
|
|
signore ? NULL : &ipo->ipo_src, ddst, NULL);
|
|
|
|
/* FALLTHROUGH */
|
|
case IPSP_IPSEC_USE:
|
|
return ipsp_spd_inp(m, seclevel, ipo, tdbout);
|
|
}
|
|
} else { /* IPSP_DIRECTION_IN */
|
|
if (tdbin != NULL) {
|
|
/*
|
|
* Special case for bundled IPcomp/ESP SAs:
|
|
* 1) only IPcomp flows are loaded into kernel
|
|
* 2) input processing processes ESP SA first
|
|
* 3) then optional IPcomp processing happens
|
|
* 4) we only update m_tag for ESP
|
|
* => 'tdbin' is always set to ESP SA
|
|
* => flow has ipo_proto for IPcomp
|
|
* So if 'tdbin' points to an ESP SA and this 'tdbin' is
|
|
* bundled with an IPcomp SA, then we replace 'tdbin'
|
|
* with the IPcomp SA at tdbin->tdb_inext.
|
|
*/
|
|
if (ipo->ipo_sproto == IPPROTO_IPCOMP &&
|
|
tdbin->tdb_sproto == IPPROTO_ESP &&
|
|
tdbin->tdb_inext != NULL &&
|
|
tdbin->tdb_inext->tdb_sproto == IPPROTO_IPCOMP)
|
|
tdbin = tdbin->tdb_inext;
|
|
|
|
/* Direct match in the cache. */
|
|
mtx_enter(&ipo_tdb_mtx);
|
|
if (ipo->ipo_tdb == tdbin) {
|
|
error = ipsp_spd_inp(m, seclevel, ipo, tdbout);
|
|
mtx_leave(&ipo_tdb_mtx);
|
|
return error;
|
|
}
|
|
mtx_leave(&ipo_tdb_mtx);
|
|
|
|
if (memcmp(dignore ? &ssrc : &ipo->ipo_dst,
|
|
&tdbin->tdb_src, tdbin->tdb_src.sa.sa_len) ||
|
|
(ipo->ipo_sproto != tdbin->tdb_sproto))
|
|
goto nomatchin;
|
|
|
|
/* Match source/dest IDs. */
|
|
if (ipo->ipo_ids)
|
|
if (tdbin->tdb_ids == NULL ||
|
|
!ipsp_ids_match(ipo->ipo_ids,
|
|
tdbin->tdb_ids))
|
|
goto nomatchin;
|
|
|
|
/* Add it to the cache. */
|
|
mtx_enter(&ipo_tdb_mtx);
|
|
if (ipo->ipo_tdb != NULL) {
|
|
TAILQ_REMOVE(&ipo->ipo_tdb->tdb_policy_head,
|
|
ipo, ipo_tdb_next);
|
|
tdb_unref(ipo->ipo_tdb);
|
|
}
|
|
ipo->ipo_tdb = tdb_ref(tdbin);
|
|
TAILQ_INSERT_TAIL(&tdbin->tdb_policy_head, ipo,
|
|
ipo_tdb_next);
|
|
error = ipsp_spd_inp(m, seclevel, ipo, tdbout);
|
|
mtx_leave(&ipo_tdb_mtx);
|
|
return error;
|
|
|
|
nomatchin: /* Nothing needed here, falling through */
|
|
;
|
|
}
|
|
|
|
/* Check whether cached entry applies. */
|
|
mtx_enter(&ipo_tdb_mtx);
|
|
if (ipo->ipo_tdb != NULL) {
|
|
/*
|
|
* We only need to check that the correct
|
|
* security protocol and security gateway are
|
|
* set; IDs will be the same since the cached
|
|
* entry is linked on this policy.
|
|
*/
|
|
if (ipo->ipo_sproto == ipo->ipo_tdb->tdb_sproto &&
|
|
!memcmp(&ipo->ipo_tdb->tdb_src,
|
|
dignore ? &ssrc : &ipo->ipo_dst,
|
|
ipo->ipo_tdb->tdb_src.sa.sa_len))
|
|
goto skipinputsearch;
|
|
|
|
/* Not applicable, unlink. */
|
|
TAILQ_REMOVE(&ipo->ipo_tdb->tdb_policy_head, ipo,
|
|
ipo_tdb_next);
|
|
tdb_unref(ipo->ipo_tdb);
|
|
ipo->ipo_tdb = NULL;
|
|
ipo->ipo_last_searched = 0;
|
|
}
|
|
|
|
/* Find whether there exists an appropriate SA. */
|
|
if (ipo->ipo_last_searched <= ipsec_last_added) {
|
|
struct tdb *tdbp_new;
|
|
|
|
if (dignore == 0)
|
|
ipo->ipo_last_searched = getuptime();
|
|
|
|
/* gettdb() takes tdb_sadb_mtx, preserve lock order */
|
|
mtx_leave(&ipo_tdb_mtx);
|
|
tdbp_new = gettdbbysrc(rdomain,
|
|
dignore ? &ssrc : &ipo->ipo_dst,
|
|
ipo->ipo_sproto, ipo->ipo_ids,
|
|
&ipo->ipo_addr, &ipo->ipo_mask);
|
|
mtx_enter(&ipo_tdb_mtx);
|
|
if ((tdbp_new != NULL) &&
|
|
(tdbp_new->tdb_flags & TDBF_DELETED)) {
|
|
/*
|
|
* After tdb_delete() has released ipo_tdb_mtx
|
|
* in tdb_unlink(), never add a new one.
|
|
* tdb_cleanspd() has to catch all of them.
|
|
*/
|
|
tdb_unref(tdbp_new);
|
|
tdbp_new = NULL;
|
|
}
|
|
if (ipo->ipo_tdb != NULL) {
|
|
/* Remove cached TDB from parallel thread. */
|
|
TAILQ_REMOVE(&ipo->ipo_tdb->tdb_policy_head,
|
|
ipo, ipo_tdb_next);
|
|
tdb_unref(ipo->ipo_tdb);
|
|
}
|
|
ipo->ipo_tdb = tdbp_new;
|
|
if (ipo->ipo_tdb != NULL) {
|
|
/* gettdbbysrc() has already refcounted tdb */
|
|
TAILQ_INSERT_TAIL(
|
|
&ipo->ipo_tdb->tdb_policy_head,
|
|
ipo, ipo_tdb_next);
|
|
}
|
|
}
|
|
skipinputsearch:
|
|
mtx_leave(&ipo_tdb_mtx);
|
|
|
|
switch (ipo->ipo_type) {
|
|
case IPSP_IPSEC_REQUIRE:
|
|
/* If appropriate SA exists, don't acquire another. */
|
|
if (ipo->ipo_tdb != NULL)
|
|
return -EINVAL; /* Silently drop packet. */
|
|
|
|
/* Acquire SA through key management. */
|
|
if ((error = ipsp_acquire_sa(ipo,
|
|
dignore ? &ssrc : &ipo->ipo_dst,
|
|
signore ? NULL : &ipo->ipo_src, ddst, m)) != 0)
|
|
return error;
|
|
|
|
/* FALLTHROUGH */
|
|
case IPSP_IPSEC_DONTACQ:
|
|
return -EINVAL; /* Silently drop packet. */
|
|
|
|
case IPSP_IPSEC_ACQUIRE:
|
|
/* If appropriate SA exists, don't acquire another. */
|
|
if (ipo->ipo_tdb != NULL)
|
|
return ipsp_spd_inp(m, seclevel, ipo, tdbout);
|
|
|
|
/* Acquire SA through key management. */
|
|
ipsp_acquire_sa(ipo, dignore ? &ssrc : &ipo->ipo_dst,
|
|
signore ? NULL : &ipo->ipo_src, ddst, NULL);
|
|
|
|
/* FALLTHROUGH */
|
|
case IPSP_IPSEC_USE:
|
|
return ipsp_spd_inp(m, seclevel, ipo, tdbout);
|
|
}
|
|
}
|
|
|
|
/* Shouldn't ever get this far. */
|
|
return EINVAL;
|
|
}
|
|
|
|
/*
|
|
* Delete a policy from the SPD.
|
|
*/
|
|
int
|
|
ipsec_delete_policy(struct ipsec_policy *ipo)
|
|
{
|
|
struct ipsec_acquire *ipa;
|
|
struct radix_node_head *rnh;
|
|
struct radix_node *rn = (struct radix_node *)ipo;
|
|
|
|
NET_ASSERT_LOCKED_EXCLUSIVE();
|
|
|
|
if (refcnt_rele(&ipo->ipo_refcnt) == 0)
|
|
return 0;
|
|
|
|
/* Delete from SPD. */
|
|
if ((rnh = spd_table_get(ipo->ipo_rdomain)) == NULL ||
|
|
rn_delete(&ipo->ipo_addr, &ipo->ipo_mask, rnh, rn) == NULL)
|
|
return (ESRCH);
|
|
|
|
mtx_enter(&ipo_tdb_mtx);
|
|
if (ipo->ipo_tdb != NULL) {
|
|
TAILQ_REMOVE(&ipo->ipo_tdb->tdb_policy_head, ipo,
|
|
ipo_tdb_next);
|
|
tdb_unref(ipo->ipo_tdb);
|
|
ipo->ipo_tdb = NULL;
|
|
}
|
|
mtx_leave(&ipo_tdb_mtx);
|
|
|
|
mtx_enter(&ipsec_acquire_mtx);
|
|
while ((ipa = TAILQ_FIRST(&ipo->ipo_acquires)) != NULL)
|
|
ipsp_delete_acquire_locked(ipa);
|
|
mtx_leave(&ipsec_acquire_mtx);
|
|
|
|
TAILQ_REMOVE(&ipsec_policy_head, ipo, ipo_list);
|
|
|
|
if (ipo->ipo_ids)
|
|
ipsp_ids_free(ipo->ipo_ids);
|
|
|
|
ipsec_in_use--;
|
|
|
|
pool_put(&ipsec_policy_pool, ipo);
|
|
|
|
return 0;
|
|
}
|
|
|
|
void
|
|
ipsp_delete_acquire_timer(void *v)
|
|
{
|
|
struct ipsec_acquire *ipa = v;
|
|
|
|
mtx_enter(&ipsec_acquire_mtx);
|
|
refcnt_rele(&ipa->ipa_refcnt);
|
|
ipsp_delete_acquire_locked(ipa);
|
|
mtx_leave(&ipsec_acquire_mtx);
|
|
}
|
|
|
|
/*
|
|
* Delete a pending IPsec acquire record.
|
|
*/
|
|
void
|
|
ipsp_delete_acquire(struct ipsec_acquire *ipa)
|
|
{
|
|
mtx_enter(&ipsec_acquire_mtx);
|
|
ipsp_delete_acquire_locked(ipa);
|
|
mtx_leave(&ipsec_acquire_mtx);
|
|
}
|
|
|
|
void
|
|
ipsp_delete_acquire_locked(struct ipsec_acquire *ipa)
|
|
{
|
|
if (timeout_del(&ipa->ipa_timeout) == 1)
|
|
refcnt_rele(&ipa->ipa_refcnt);
|
|
ipsp_unref_acquire_locked(ipa);
|
|
}
|
|
|
|
void
|
|
ipsec_unref_acquire(struct ipsec_acquire *ipa)
|
|
{
|
|
mtx_enter(&ipsec_acquire_mtx);
|
|
ipsp_unref_acquire_locked(ipa);
|
|
mtx_leave(&ipsec_acquire_mtx);
|
|
}
|
|
|
|
void
|
|
ipsp_unref_acquire_locked(struct ipsec_acquire *ipa)
|
|
{
|
|
MUTEX_ASSERT_LOCKED(&ipsec_acquire_mtx);
|
|
|
|
if (refcnt_rele(&ipa->ipa_refcnt) == 0)
|
|
return;
|
|
TAILQ_REMOVE(&ipsec_acquire_head, ipa, ipa_next);
|
|
TAILQ_REMOVE(&ipa->ipa_policy->ipo_acquires, ipa, ipa_ipo_next);
|
|
ipa->ipa_policy = NULL;
|
|
|
|
pool_put(&ipsec_acquire_pool, ipa);
|
|
}
|
|
|
|
/*
|
|
* Find out if there's an ACQUIRE pending.
|
|
* XXX Need a better structure.
|
|
*/
|
|
int
|
|
ipsp_pending_acquire(struct ipsec_policy *ipo, union sockaddr_union *gw)
|
|
{
|
|
struct ipsec_acquire *ipa;
|
|
|
|
NET_ASSERT_LOCKED();
|
|
|
|
mtx_enter(&ipsec_acquire_mtx);
|
|
TAILQ_FOREACH(ipa, &ipo->ipo_acquires, ipa_ipo_next) {
|
|
if (!memcmp(gw, &ipa->ipa_addr, gw->sa.sa_len))
|
|
break;
|
|
}
|
|
mtx_leave(&ipsec_acquire_mtx);
|
|
|
|
return (ipa != NULL);
|
|
}
|
|
|
|
/*
|
|
* Signal key management that we need an SA.
|
|
* XXX For outgoing policies, we could try to hold on to the mbuf.
|
|
*/
|
|
int
|
|
ipsp_acquire_sa(struct ipsec_policy *ipo, union sockaddr_union *gw,
|
|
union sockaddr_union *laddr, struct sockaddr_encap *ddst, struct mbuf *m)
|
|
{
|
|
struct ipsec_acquire *ipa;
|
|
|
|
NET_ASSERT_LOCKED();
|
|
|
|
/* Check whether request has been made already. */
|
|
if (ipsp_pending_acquire(ipo, gw))
|
|
return 0;
|
|
|
|
/* Add request in cache and proceed. */
|
|
ipa = pool_get(&ipsec_acquire_pool, PR_NOWAIT|PR_ZERO);
|
|
if (ipa == NULL)
|
|
return ENOMEM;
|
|
|
|
ipa->ipa_addr = *gw;
|
|
|
|
refcnt_init(&ipa->ipa_refcnt);
|
|
timeout_set(&ipa->ipa_timeout, ipsp_delete_acquire_timer, ipa);
|
|
|
|
ipa->ipa_info.sen_len = ipa->ipa_mask.sen_len = SENT_LEN;
|
|
ipa->ipa_info.sen_family = ipa->ipa_mask.sen_family = PF_KEY;
|
|
|
|
/* Just copy the right information. */
|
|
switch (ipo->ipo_addr.sen_type) {
|
|
case SENT_IP4:
|
|
ipa->ipa_info.sen_type = ipa->ipa_mask.sen_type = SENT_IP4;
|
|
ipa->ipa_info.sen_direction = ipo->ipo_addr.sen_direction;
|
|
ipa->ipa_mask.sen_direction = ipo->ipo_mask.sen_direction;
|
|
|
|
if (ipsp_is_unspecified(ipo->ipo_dst)) {
|
|
ipa->ipa_info.sen_ip_src = ddst->sen_ip_src;
|
|
ipa->ipa_mask.sen_ip_src.s_addr = INADDR_BROADCAST;
|
|
|
|
ipa->ipa_info.sen_ip_dst = ddst->sen_ip_dst;
|
|
ipa->ipa_mask.sen_ip_dst.s_addr = INADDR_BROADCAST;
|
|
} else {
|
|
ipa->ipa_info.sen_ip_src = ipo->ipo_addr.sen_ip_src;
|
|
ipa->ipa_mask.sen_ip_src = ipo->ipo_mask.sen_ip_src;
|
|
|
|
ipa->ipa_info.sen_ip_dst = ipo->ipo_addr.sen_ip_dst;
|
|
ipa->ipa_mask.sen_ip_dst = ipo->ipo_mask.sen_ip_dst;
|
|
}
|
|
|
|
ipa->ipa_info.sen_proto = ipo->ipo_addr.sen_proto;
|
|
ipa->ipa_mask.sen_proto = ipo->ipo_mask.sen_proto;
|
|
|
|
if (ipo->ipo_addr.sen_proto) {
|
|
ipa->ipa_info.sen_sport = ipo->ipo_addr.sen_sport;
|
|
ipa->ipa_mask.sen_sport = ipo->ipo_mask.sen_sport;
|
|
|
|
ipa->ipa_info.sen_dport = ipo->ipo_addr.sen_dport;
|
|
ipa->ipa_mask.sen_dport = ipo->ipo_mask.sen_dport;
|
|
}
|
|
break;
|
|
|
|
#ifdef INET6
|
|
case SENT_IP6:
|
|
ipa->ipa_info.sen_type = ipa->ipa_mask.sen_type = SENT_IP6;
|
|
ipa->ipa_info.sen_ip6_direction =
|
|
ipo->ipo_addr.sen_ip6_direction;
|
|
ipa->ipa_mask.sen_ip6_direction =
|
|
ipo->ipo_mask.sen_ip6_direction;
|
|
|
|
if (ipsp_is_unspecified(ipo->ipo_dst)) {
|
|
ipa->ipa_info.sen_ip6_src = ddst->sen_ip6_src;
|
|
ipa->ipa_mask.sen_ip6_src = in6mask128;
|
|
|
|
ipa->ipa_info.sen_ip6_dst = ddst->sen_ip6_dst;
|
|
ipa->ipa_mask.sen_ip6_dst = in6mask128;
|
|
} else {
|
|
ipa->ipa_info.sen_ip6_src = ipo->ipo_addr.sen_ip6_src;
|
|
ipa->ipa_mask.sen_ip6_src = ipo->ipo_mask.sen_ip6_src;
|
|
|
|
ipa->ipa_info.sen_ip6_dst = ipo->ipo_addr.sen_ip6_dst;
|
|
ipa->ipa_mask.sen_ip6_dst = ipo->ipo_mask.sen_ip6_dst;
|
|
}
|
|
|
|
ipa->ipa_info.sen_ip6_proto = ipo->ipo_addr.sen_ip6_proto;
|
|
ipa->ipa_mask.sen_ip6_proto = ipo->ipo_mask.sen_ip6_proto;
|
|
|
|
if (ipo->ipo_mask.sen_ip6_proto) {
|
|
ipa->ipa_info.sen_ip6_sport =
|
|
ipo->ipo_addr.sen_ip6_sport;
|
|
ipa->ipa_mask.sen_ip6_sport =
|
|
ipo->ipo_mask.sen_ip6_sport;
|
|
ipa->ipa_info.sen_ip6_dport =
|
|
ipo->ipo_addr.sen_ip6_dport;
|
|
ipa->ipa_mask.sen_ip6_dport =
|
|
ipo->ipo_mask.sen_ip6_dport;
|
|
}
|
|
break;
|
|
#endif /* INET6 */
|
|
|
|
default:
|
|
pool_put(&ipsec_acquire_pool, ipa);
|
|
return 0;
|
|
}
|
|
|
|
mtx_enter(&ipsec_acquire_mtx);
|
|
#ifdef IPSEC
|
|
if (timeout_add_sec(&ipa->ipa_timeout, ipsec_expire_acquire) == 1)
|
|
refcnt_take(&ipa->ipa_refcnt);
|
|
#endif
|
|
TAILQ_INSERT_TAIL(&ipsec_acquire_head, ipa, ipa_next);
|
|
TAILQ_INSERT_TAIL(&ipo->ipo_acquires, ipa, ipa_ipo_next);
|
|
ipa->ipa_policy = ipo;
|
|
mtx_leave(&ipsec_acquire_mtx);
|
|
|
|
/* PF_KEYv2 notification message. */
|
|
return pfkeyv2_acquire(ipo, gw, laddr, &ipa->ipa_seq, ddst);
|
|
}
|
|
|
|
/*
|
|
* Deal with PCB security requirements.
|
|
*/
|
|
int
|
|
ipsp_spd_inp(struct mbuf *m, const struct ipsec_level *seclevel,
|
|
struct ipsec_policy *ipo, struct tdb **tdbout)
|
|
{
|
|
/* Sanity check. */
|
|
if (seclevel == NULL)
|
|
goto justreturn;
|
|
|
|
/* We only support IPSEC_LEVEL_BYPASS or IPSEC_LEVEL_AVAIL */
|
|
|
|
if (seclevel->sl_esp_trans == IPSEC_LEVEL_BYPASS &&
|
|
seclevel->sl_esp_network == IPSEC_LEVEL_BYPASS &&
|
|
seclevel->sl_auth == IPSEC_LEVEL_BYPASS)
|
|
goto justreturn;
|
|
|
|
if (seclevel->sl_esp_trans == IPSEC_LEVEL_AVAIL &&
|
|
seclevel->sl_esp_network == IPSEC_LEVEL_AVAIL &&
|
|
seclevel->sl_auth == IPSEC_LEVEL_AVAIL)
|
|
goto justreturn;
|
|
|
|
return -EINVAL; /* Silently drop packet. */
|
|
|
|
justreturn:
|
|
if (tdbout != NULL) {
|
|
if (ipo != NULL)
|
|
*tdbout = tdb_ref(ipo->ipo_tdb);
|
|
else
|
|
*tdbout = NULL;
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
/*
|
|
* Find a pending ACQUIRE record based on its sequence number.
|
|
* XXX Need to use a better data structure.
|
|
*/
|
|
struct ipsec_acquire *
|
|
ipsec_get_acquire(u_int32_t seq)
|
|
{
|
|
struct ipsec_acquire *ipa;
|
|
|
|
NET_ASSERT_LOCKED();
|
|
|
|
mtx_enter(&ipsec_acquire_mtx);
|
|
TAILQ_FOREACH(ipa, &ipsec_acquire_head, ipa_next) {
|
|
if (ipa->ipa_seq == seq) {
|
|
refcnt_take(&ipa->ipa_refcnt);
|
|
break;
|
|
}
|
|
}
|
|
mtx_leave(&ipsec_acquire_mtx);
|
|
|
|
return ipa;
|
|
}
|