#include "pf.h"
#include <sys/param.h>
#include <sys/systm.h>
#include <sys/mbuf.h>
#include <sys/socket.h>
#include <sys/timeout.h>
#include <net/if.h>
#include <net/route.h>
#include <netinet/in.h>
#include <netinet/ip.h>
#include <netinet/ip6.h>
#include <netinet/ip_ipsp.h>
#include <netinet/ip_var.h>
#include <netinet6/ip6_var.h>
#if NPF > 0
#include <net/pfvar.h>
#endif
#include <netinet/udp.h>
#include <netinet/ip_ipip.h>
#include <netinet/ip_ah.h>
#include <netinet/ip_esp.h>
#include <netinet/ip_ipcomp.h>
#include <crypto/xform.h>
#ifdef ENCDEBUG
#define DPRINTF(fmt, args...) \
do { \
if (atomic_load_int(&encdebug)) \
printf("%s: " fmt "\n", __func__, ## args); \
} while (0)
#else
#define DPRINTF(fmt, args...) \
do { } while (0)
#endif
int udpencap_enable = 1;
int udpencap_port = 4500;
int
ipsp_process_packet(struct mbuf *m, struct tdb *tdb, int af, int tunalready,
int setdf)
{
int hlen, off, error;
#ifdef INET6
struct ip6_ext ip6e;
int nxt;
int dstopt = 0;
#endif
struct ip *ip;
#ifdef INET6
struct ip6_hdr *ip6;
#endif
#ifdef ENCDEBUG
char buf[INET6_ADDRSTRLEN];
#endif
if ((tdb->tdb_sproto == IPPROTO_ESP && !atomic_load_int(&esp_enable)) ||
(tdb->tdb_sproto == IPPROTO_AH && !atomic_load_int(&ah_enable)) ||
(tdb->tdb_sproto == IPPROTO_IPCOMP &&
!atomic_load_int(&ipcomp_enable))) {
DPRINTF("IPsec outbound packet dropped due to policy "
"(check your sysctls)");
error = EHOSTUNREACH;
goto drop;
}
if (!tdb->tdb_xform) {
DPRINTF("uninitialized TDB");
error = EHOSTUNREACH;
goto drop;
}
if (tdb->tdb_flags & TDBF_INVALID) {
DPRINTF("attempt to use invalid SA %s/%08x/%u",
ipsp_address(&tdb->tdb_dst, buf, sizeof(buf)),
ntohl(tdb->tdb_spi), tdb->tdb_sproto);
error = ENXIO;
goto drop;
}
switch (tdb->tdb_dst.sa.sa_family) {
case AF_INET:
break;
#ifdef INET6
case AF_INET6:
break;
#endif
default:
DPRINTF("attempt to use SA %s/%08x/%u for protocol family %d",
ipsp_address(&tdb->tdb_dst, buf, sizeof(buf)),
ntohl(tdb->tdb_spi), tdb->tdb_sproto,
tdb->tdb_dst.sa.sa_family);
error = EPFNOSUPPORT;
goto drop;
}
if (tdb->tdb_first_use == 0) {
tdb->tdb_first_use = gettime();
if (tdb->tdb_flags & TDBF_FIRSTUSE) {
if (timeout_add_sec(&tdb->tdb_first_tmo,
tdb->tdb_exp_first_use))
tdb_ref(tdb);
}
if (tdb->tdb_flags & TDBF_SOFT_FIRSTUSE) {
if (timeout_add_sec(&tdb->tdb_sfirst_tmo,
tdb->tdb_soft_first_use))
tdb_ref(tdb);
}
}
if (tunalready == 0) {
if (af == tdb->tdb_dst.sa.sa_family) {
switch (af) {
case AF_INET:
hlen = sizeof(struct ip);
break;
#ifdef INET6
case AF_INET6:
hlen = sizeof(struct ip6_hdr);
break;
#endif
}
if (m->m_len < hlen) {
if ((m = m_pullup(m, hlen)) == NULL) {
error = ENOBUFS;
goto drop;
}
}
if (af == AF_INET) {
ip = mtod(m, struct ip *);
if (setdf == IPSP_DF_INHERIT) {
setdf =
ISSET(ip->ip_off, htons(IP_DF)) ?
IPSP_DF_ON : IPSP_DF_OFF;
}
}
#ifdef INET6
if (af == AF_INET6)
ip6 = mtod(m, struct ip6_hdr *);
#endif
}
if ((tdb->tdb_dst.sa.sa_family != af) ||
(tdb->tdb_flags & TDBF_TUNNELING) ||
(tdb->tdb_xform->xf_type == XF_IP4) ||
((tdb->tdb_dst.sa.sa_family == AF_INET) &&
(tdb->tdb_dst.sin.sin_addr.s_addr != INADDR_ANY) &&
(tdb->tdb_dst.sin.sin_addr.s_addr != ip->ip_dst.s_addr)) ||
#ifdef INET6
((tdb->tdb_dst.sa.sa_family == AF_INET6) &&
(!IN6_IS_ADDR_UNSPECIFIED(&tdb->tdb_dst.sin6.sin6_addr)) &&
(!IN6_ARE_ADDR_EQUAL(&tdb->tdb_dst.sin6.sin6_addr,
&ip6->ip6_dst))) ||
#endif
0) {
if (af == AF_INET) {
if (m->m_len < sizeof(struct ip))
if ((m = m_pullup(m,
sizeof(struct ip))) == NULL) {
error = ENOBUFS;
goto drop;
}
ip = mtod(m, struct ip *);
ip->ip_len = htons(m->m_pkthdr.len);
ip->ip_sum = 0;
ip->ip_sum = in_cksum(m, ip->ip_hl << 2);
}
#ifdef INET6
if (af == AF_INET6) {
if (m->m_len < sizeof(struct ip6_hdr))
if ((m = m_pullup(m,
sizeof(struct ip6_hdr))) == NULL) {
error = ENOBUFS;
goto drop;
}
if (m->m_pkthdr.len - sizeof(*ip6) >
IPV6_MAXPACKET) {
error = ENXIO;
goto drop;
}
ip6 = mtod(m, struct ip6_hdr *);
ip6->ip6_plen = htons(m->m_pkthdr.len
- sizeof(*ip6));
}
#endif
error = ipip_output(&m, tdb);
if ((m == NULL) && (!error))
error = EFAULT;
if (error)
goto drop;
if (tdb->tdb_dst.sa.sa_family == AF_INET &&
setdf == IPSP_DF_ON) {
if (m->m_len < sizeof(struct ip))
if ((m = m_pullup(m,
sizeof(struct ip))) == NULL) {
error = ENOBUFS;
goto drop;
}
ip = mtod(m, struct ip *);
ip->ip_off |= htons(IP_DF);
}
mtx_enter(&tdb->tdb_mtx);
tdb->tdb_flags |= TDBF_USEDTUNNEL;
mtx_leave(&tdb->tdb_mtx);
}
}
if (tdb->tdb_xform->xf_type == XF_IP4)
return ipsp_process_done(m, tdb);
switch (tdb->tdb_dst.sa.sa_family) {
case AF_INET:
ip = mtod(m, struct ip *);
hlen = ip->ip_hl << 2;
off = offsetof(struct ip, ip_p);
break;
#ifdef INET6
case AF_INET6:
ip6 = mtod(m, struct ip6_hdr *);
hlen = sizeof(struct ip6_hdr);
off = offsetof(struct ip6_hdr, ip6_nxt);
nxt = ip6->ip6_nxt;
do {
switch (nxt) {
case IPPROTO_AH:
case IPPROTO_ESP:
case IPPROTO_IPCOMP:
goto exitip6loop;
case IPPROTO_HOPOPTS:
case IPPROTO_DSTOPTS:
case IPPROTO_ROUTING:
if (nxt == IPPROTO_DSTOPTS && dstopt)
goto exitip6loop;
if (nxt == IPPROTO_DSTOPTS) {
dstopt = 1;
} else if (nxt == IPPROTO_ROUTING) {
dstopt = 2;
}
if (m->m_pkthdr.len < hlen + sizeof(ip6e)) {
error = EINVAL;
goto drop;
}
m_copydata(m, hlen, sizeof(ip6e),
(caddr_t)&ip6e);
nxt = ip6e.ip6e_nxt;
off = hlen + offsetof(struct ip6_ext, ip6e_nxt);
hlen += (ip6e.ip6e_len + 1) << 3;
break;
default:
goto exitip6loop;
}
} while (hlen < m->m_pkthdr.len);
exitip6loop:
break;
#endif
default:
error = EPFNOSUPPORT;
goto drop;
}
if (m->m_pkthdr.len < hlen) {
error = EINVAL;
goto drop;
}
ipsecstat_add(ipsec_ouncompbytes, m->m_pkthdr.len);
tdbstat_add(tdb, tdb_ouncompbytes, m->m_pkthdr.len);
if (tdb->tdb_sproto == IPPROTO_IPCOMP) {
if ((m->m_pkthdr.len - hlen) < tdb->tdb_compalgxform->minlen) {
ipcompstat_inc(ipcomps_minlen);
return ipsp_process_done(m, tdb);
}
}
return (*(tdb->tdb_xform->xf_output))(m, tdb, hlen, off);
drop:
m_freem(m);
return error;
}
int
ipsp_process_done(struct mbuf *m, struct tdb *tdb)
{
struct ip *ip;
#ifdef INET6
struct ip6_hdr *ip6;
#endif
struct tdb *tdbo;
struct tdb_ident *tdbi;
struct m_tag *mtag;
int roff, error;
NET_ASSERT_LOCKED();
tdb->tdb_last_used = gettime();
if ((tdb->tdb_flags & TDBF_UDPENCAP) != 0) {
struct mbuf *mi;
struct udphdr *uh;
int iphlen;
int udpencap_port_local = atomic_load_int(&udpencap_port);
if (!atomic_load_int(&udpencap_enable) ||
!udpencap_port_local) {
error = ENXIO;
goto drop;
}
switch (tdb->tdb_dst.sa.sa_family) {
case AF_INET:
iphlen = sizeof(struct ip);
break;
#ifdef INET6
case AF_INET6:
iphlen = sizeof(struct ip6_hdr);
break;
#endif
default:
DPRINTF("unknown protocol family (%d)",
tdb->tdb_dst.sa.sa_family);
error = EPFNOSUPPORT;
goto drop;
}
mi = m_makespace(m, iphlen, sizeof(struct udphdr), &roff);
if (mi == NULL) {
error = ENOMEM;
goto drop;
}
uh = (struct udphdr *)(mtod(mi, caddr_t) + roff);
uh->uh_sport = uh->uh_dport = htons(udpencap_port_local);
if (tdb->tdb_udpencap_port)
uh->uh_dport = tdb->tdb_udpencap_port;
uh->uh_ulen = htons(m->m_pkthdr.len - iphlen);
uh->uh_sum = 0;
#ifdef INET6
if (tdb->tdb_dst.sa.sa_family == AF_INET6)
m->m_pkthdr.csum_flags |= M_UDP_CSUM_OUT;
#endif
espstat_inc(esps_udpencout);
}
switch (tdb->tdb_dst.sa.sa_family) {
case AF_INET:
ip = mtod(m, struct ip *);
ip->ip_len = htons(m->m_pkthdr.len);
if ((tdb->tdb_flags & TDBF_UDPENCAP) != 0)
ip->ip_p = IPPROTO_UDP;
break;
#ifdef INET6
case AF_INET6:
if (m->m_pkthdr.len < sizeof(*ip6)) {
error = ENXIO;
goto drop;
}
if (m->m_pkthdr.len - sizeof(*ip6) > IPV6_MAXPACKET) {
error = ENXIO;
goto drop;
}
ip6 = mtod(m, struct ip6_hdr *);
ip6->ip6_plen = htons(m->m_pkthdr.len - sizeof(*ip6));
if ((tdb->tdb_flags & TDBF_UDPENCAP) != 0)
ip6->ip6_nxt = IPPROTO_UDP;
break;
#endif
default:
DPRINTF("unknown protocol family (%d)",
tdb->tdb_dst.sa.sa_family);
error = EPFNOSUPPORT;
goto drop;
}
mtag = m_tag_get(PACKET_TAG_IPSEC_OUT_DONE, sizeof(struct tdb_ident),
M_NOWAIT);
if (mtag == NULL) {
DPRINTF("could not allocate packet tag");
error = ENOMEM;
goto drop;
}
tdbi = (struct tdb_ident *)(mtag + 1);
tdbi->dst = tdb->tdb_dst;
tdbi->proto = tdb->tdb_sproto;
tdbi->spi = tdb->tdb_spi;
tdbi->rdomain = tdb->tdb_rdomain;
m_tag_prepend(m, mtag);
ipsecstat_pkt(ipsec_opackets, ipsec_obytes, m->m_pkthdr.len);
tdbstat_pkt(tdb, tdb_opackets, tdb_obytes, m->m_pkthdr.len);
tdbo = tdb_ref(tdb->tdb_onext);
if (tdbo != NULL) {
KERNEL_ASSERT_LOCKED();
error = ipsp_process_packet(m, tdbo,
tdb->tdb_dst.sa.sa_family, 0, IPSP_DF_INHERIT);
tdb_unref(tdbo);
return error;
}
#if NPF > 0
pf_tag_packet(m, tdb->tdb_tag, -1);
pf_pkt_addr_changed(m);
#endif
if (tdb->tdb_rdomain != tdb->tdb_rdomain_post)
m->m_pkthdr.ph_rtableid = tdb->tdb_rdomain_post;
switch (tdb->tdb_dst.sa.sa_family) {
case AF_INET:
error = ip_output(m, NULL, NULL, IP_RAWOUTPUT, NULL, NULL, 0);
break;
#ifdef INET6
case AF_INET6:
error = ip6_output(m, NULL, NULL, 0, NULL, NULL);
break;
#endif
default:
error = EPFNOSUPPORT;
break;
}
return error;
drop:
m_freem(m);
return error;
}
ssize_t
ipsec_hdrsz(struct tdb *tdbp)
{
ssize_t adjust;
switch (tdbp->tdb_sproto) {
case IPPROTO_IPIP:
adjust = 0;
break;
case IPPROTO_ESP:
if (tdbp->tdb_encalgxform == NULL)
return (-1);
adjust = 2 * sizeof(u_int32_t) + tdbp->tdb_ivlen;
if (tdbp->tdb_flags & TDBF_UDPENCAP)
adjust += sizeof(struct udphdr);
if (tdbp->tdb_authalgxform != NULL)
adjust += tdbp->tdb_authalgxform->authsize;
adjust += MAX(4, tdbp->tdb_encalgxform->blocksize);
break;
case IPPROTO_AH:
if (tdbp->tdb_authalgxform == NULL)
return (-1);
adjust = AH_FLENGTH + sizeof(u_int32_t);
adjust += tdbp->tdb_authalgxform->authsize;
break;
default:
return (-1);
}
if (!(tdbp->tdb_flags & TDBF_TUNNELING) &&
!(tdbp->tdb_flags & TDBF_USEDTUNNEL))
return (adjust);
switch (tdbp->tdb_dst.sa.sa_family) {
case AF_INET:
adjust += sizeof(struct ip);
break;
#ifdef INET6
case AF_INET6:
adjust += sizeof(struct ip6_hdr);
break;
#endif
}
return (adjust);
}
void
ipsec_adjust_mtu(struct mbuf *m, u_int32_t mtu)
{
struct tdb_ident *tdbi;
struct tdb *tdbp;
struct m_tag *mtag;
ssize_t adjust;
NET_ASSERT_LOCKED();
for (mtag = m_tag_find(m, PACKET_TAG_IPSEC_OUT_DONE, NULL); mtag;
mtag = m_tag_find(m, PACKET_TAG_IPSEC_OUT_DONE, mtag)) {
tdbi = (struct tdb_ident *)(mtag + 1);
tdbp = gettdb(tdbi->rdomain, tdbi->spi, &tdbi->dst,
tdbi->proto);
if (tdbp == NULL)
break;
if ((adjust = ipsec_hdrsz(tdbp)) == -1) {
tdb_unref(tdbp);
break;
}
mtu -= adjust;
tdbp->tdb_mtu = mtu;
tdbp->tdb_mtutimeout = gettime() +
atomic_load_int(&ip_mtudisc_timeout);
DPRINTF("spi %08x mtu %d adjust %ld mbuf %p",
ntohl(tdbp->tdb_spi), tdbp->tdb_mtu, adjust, m);
tdb_unref(tdbp);
}
}