1008 lines
30 KiB
C
1008 lines
30 KiB
C
/*
|
|
* This file is part of capwap-mitm.
|
|
*
|
|
* Foobar is free software: you can redistribute it and/or modify
|
|
* it under the terms of the GNU General Public License as published by
|
|
* the Free Software Foundation, either version 3 of the License, or
|
|
* (at your option) any later version.
|
|
*
|
|
* capwap-mitm is distributed in the hope that it will be useful,
|
|
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
* GNU General Public License for more details.
|
|
*
|
|
* You should have received a copy of the GNU General Public License
|
|
* along with capwap-mitm. If not, see <http://www.gnu.org/licenses/>.
|
|
*/
|
|
|
|
#ifdef HAVE_CONFIG_H
|
|
#include "config.h"
|
|
#endif
|
|
|
|
#define _REENTRANT
|
|
|
|
#include <assert.h>
|
|
|
|
#include <stdlib.h>
|
|
#include <string.h>
|
|
#include <unistd.h>
|
|
#include <fcntl.h>
|
|
#include <stdio.h>
|
|
#include <inttypes.h>
|
|
#include <errno.h>
|
|
#include <sys/mman.h>
|
|
#include <sys/queue.h>
|
|
#include <sys/resource.h>
|
|
#include <sys/time.h>
|
|
#include <arpa/inet.h>
|
|
#include <getopt.h>
|
|
#include <sys/tree.h>
|
|
#include <netinet/ip.h>
|
|
#include <netinet/ip6.h>
|
|
#include <netinet/udp.h>
|
|
|
|
#include <ev.h>
|
|
#include <gnutls/gnutls.h>
|
|
#include <gnutls/dtls.h>
|
|
|
|
#include <pcap/bpf.h>
|
|
#include <pcap/pcap.h>
|
|
|
|
#include "log.h"
|
|
|
|
static const char _ident[] = "capwap-mitm v" VERSION;
|
|
static const char _build[] = "build on " __DATE__ " " __TIME__ " with gcc " __VERSION__;
|
|
|
|
#define MAX_BUFFER 2048
|
|
|
|
struct sockaddr_storage server_addr;
|
|
struct sockaddr_storage listen_addr;
|
|
|
|
#if !defined(CERTSDIR)
|
|
#define CERTSDIR "certs"
|
|
#endif
|
|
|
|
const char *dtls_server_keyfile = CERTSDIR "/server.key";
|
|
const char *dtls_server_certfile = CERTSDIR "/server.pem";
|
|
const char *dtls_client_keyfile = CERTSDIR "/client.key";
|
|
const char *dtls_client_certfile = CERTSDIR "/client.pem";
|
|
const char *dtls_cafile = CERTSDIR "/cacerts.pem";
|
|
const char *dtls_crlfile = CERTSDIR "/crl.pem";
|
|
|
|
const char *pcap_fname = NULL;
|
|
pcap_dumper_t *dumper = NULL;
|
|
|
|
gnutls_certificate_credentials_t x509_server_cred;
|
|
gnutls_certificate_credentials_t x509_client_cred;
|
|
gnutls_priority_t priority_cache;
|
|
gnutls_dh_params_t dh_server_params;
|
|
gnutls_dh_params_t dh_client_params;
|
|
gnutls_datum_t cookie_key;
|
|
|
|
pcap_t *pcap;
|
|
|
|
struct capwap_port {
|
|
int port;
|
|
|
|
int listen_fd;
|
|
ev_io listen_ev;
|
|
};
|
|
|
|
struct buffer_q {
|
|
SIMPLEQ_ENTRY(buffer_q) entry;
|
|
|
|
ssize_t buffer_len;
|
|
unsigned char buffer[];
|
|
};
|
|
|
|
struct dtls_session {
|
|
int fd;
|
|
|
|
int is_connected;
|
|
|
|
struct sockaddr_storage peer_addr;
|
|
socklen_t peer_addrlen;
|
|
|
|
int handshake_done;
|
|
gnutls_session_t session;
|
|
|
|
SIMPLEQ_HEAD(plain_q, buffer_q) plain_q;
|
|
|
|
const unsigned char *buffer;
|
|
ssize_t buffer_len;
|
|
};
|
|
|
|
struct wtp {
|
|
struct sockaddr_storage addr;
|
|
struct capwap_port *capwap_port;
|
|
|
|
RB_ENTRY (wtp) wtp_node;
|
|
ev_timer timeout;
|
|
ev_io client_ev;
|
|
|
|
struct dtls_session server;
|
|
struct dtls_session client;
|
|
};
|
|
|
|
static ssize_t dtls_push_func(gnutls_transport_ptr_t p, const void *data, size_t size);
|
|
static ssize_t dtls_pull_func(gnutls_transport_ptr_t p, void *data, size_t size);
|
|
static int dtls_pull_timeout_func(gnutls_transport_ptr_t p, unsigned int ms);
|
|
|
|
static int wtp_addr_compare(struct wtp *a, struct wtp *b);
|
|
|
|
RB_HEAD(wtp_tree, wtp) wtp_tree;
|
|
RB_PROTOTYPE(wtp_tree, wtp, wtp_node, wtp_addr_compare);
|
|
RB_GENERATE(wtp_tree, wtp, wtp_node, wtp_addr_compare);
|
|
|
|
#if !defined(offsetof)
|
|
#define offsetof(type, member) __builtin_offsetof (type, member)
|
|
#endif
|
|
#define container_of(var, type, member) (type *)(((unsigned char *)var) - offsetof(type, member))
|
|
|
|
#define SIN_ADDR_PTR(addr) ((((struct sockaddr *)(addr))->sa_family == AF_INET) ? (void *)&(((struct sockaddr_in *)(addr))->sin_addr) : (void *)&(((struct sockaddr_in6 *)(addr))->sin6_addr))
|
|
#define SIN_PORT(addr) ((((struct sockaddr *)(addr))->sa_family == AF_INET) ? (((struct sockaddr_in *)(addr))->sin_port) : (((struct sockaddr_in6 *)(addr))->sin6_port))
|
|
|
|
#define INT_CMP(A, B) \
|
|
({ \
|
|
typeof(A) a_ = (A); \
|
|
typeof(B) b_ = (B); \
|
|
a_ < b_ ? -1 : (a_ > b_ ? 1 : 0); \
|
|
})
|
|
|
|
#define SOCK_ADDR_CMP(a, b, socktype, field) \
|
|
memcmp(&(((struct socktype *)(a))->field), \
|
|
&(((struct socktype *)(b))->field), \
|
|
sizeof(((struct socktype *)(a))->field))
|
|
|
|
#define SOCK_PORT_CMP(a, b, socktype, field) \
|
|
INT_CMP(((struct socktype *)(a))->field, ((struct socktype *)(b))->field)
|
|
|
|
static int wtp_addr_compare(struct wtp *a, struct wtp *b)
|
|
{
|
|
int r;
|
|
|
|
if ((r = INT_CMP(a->addr.ss_family, b->addr.ss_family)) != 0)
|
|
return r;
|
|
|
|
switch (a->addr.ss_family) {
|
|
case AF_INET:
|
|
if ((r = SOCK_ADDR_CMP(&a->addr, &b->addr, sockaddr_in, sin_addr)) != 0)
|
|
return r;
|
|
return SOCK_PORT_CMP(&a->addr, &b->addr, sockaddr_in, sin_port);
|
|
|
|
case AF_INET6:
|
|
if ((r = SOCK_ADDR_CMP(&a->addr, &b->addr, sockaddr_in6, sin6_addr)) != 0)
|
|
return r;
|
|
return SOCK_PORT_CMP(&a->addr, &b->addr, sockaddr_in6, sin6_port);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
static uint32_t cksum_part(uint8_t *ip, int len, uint32_t sum)
|
|
{
|
|
while (len > 1) {
|
|
sum += *(uint16_t *)ip;
|
|
if (sum & 0x80000000) /* if high order bit set, fold */
|
|
sum = (sum & 0xFFFF) + (sum >> 16);
|
|
len -= 2;
|
|
ip += 2;
|
|
}
|
|
|
|
if (len) /* take care of left over byte */
|
|
sum += (uint16_t) *(uint8_t *)ip;
|
|
|
|
return sum;
|
|
}
|
|
|
|
static uint16_t cksum_finish(uint32_t sum)
|
|
{
|
|
while (sum >> 16)
|
|
sum = (sum & 0xFFFF) + (sum >> 16);
|
|
|
|
return ~sum;
|
|
}
|
|
|
|
static uint16_t cksum(uint8_t *ip, int len)
|
|
{
|
|
return cksum_finish(cksum_part(ip, len, 0));
|
|
}
|
|
|
|
static void capwap_dump(struct sockaddr *src, struct sockaddr *dst, const unsigned char *buffer, ssize_t len)
|
|
{
|
|
char ipaddr[INET6_ADDRSTRLEN] __attribute__((unused));
|
|
|
|
if (!dumper)
|
|
return;
|
|
|
|
debug("src: %d, dst: %d", src->sa_family, dst->sa_family);
|
|
/*
|
|
#if defined(DEBUG)
|
|
inet_ntop(s->peer_addr.ss_family, SIN_ADDR_PTR(&s->peer_addr), ipaddr, sizeof(ipaddr));
|
|
debug("DTLS PUSH on %d to IP: %s:%d, len: %zd\n", s->fd, ipaddr, ntohs(SIN_PORT(&s->peer_addr)), size);
|
|
#endif
|
|
*/
|
|
|
|
if (src->sa_family == AF_INET) {
|
|
struct pcap_pkthdr hdr;
|
|
unsigned char *pkt = alloca(sizeof(struct iphdr) + sizeof(struct udphdr) + len);
|
|
struct iphdr *iph = (struct iphdr *)pkt;
|
|
struct udphdr *udph = (struct udphdr *)(iph + 1);
|
|
|
|
memset(pkt, 0, sizeof(struct iphdr) + sizeof(struct udphdr) + len);
|
|
|
|
iph->version = 4;
|
|
iph->ihl = sizeof(struct iphdr) / 4;
|
|
iph->protocol = IPPROTO_UDP;
|
|
iph->tot_len = htons(sizeof(struct iphdr) + sizeof(struct udphdr) + len);
|
|
iph->ttl = 64;
|
|
iph->saddr = ((struct sockaddr_in *)src)->sin_addr.s_addr;
|
|
iph->daddr = ((struct sockaddr_in *)dst)->sin_addr.s_addr;
|
|
iph->check = cksum((uint8_t *)iph, sizeof(struct iphdr));
|
|
|
|
udph->source = ((struct sockaddr_in *)src)->sin_port;
|
|
udph->dest = ((struct sockaddr_in *)dst)->sin_port;
|
|
udph->len = htons(sizeof(struct udphdr) + len);
|
|
|
|
memcpy(udph + 1, buffer, len);
|
|
|
|
udph->check = cksum((uint8_t *)udph, sizeof(struct udphdr) + len);
|
|
|
|
memset(&hdr, 0, sizeof(hdr));
|
|
gettimeofday(&hdr.ts, NULL);
|
|
hdr.caplen = hdr.len = sizeof(struct iphdr) + sizeof(struct udphdr) + len;
|
|
|
|
pcap_dump((u_char *)dumper, &hdr, pkt);
|
|
} else {
|
|
struct pcap_pkthdr hdr;
|
|
unsigned char *pkt = alloca(sizeof(struct ip6_hdr) + sizeof(struct udphdr) + len);
|
|
struct ip6_hdr *iph = (struct ip6_hdr *)pkt;
|
|
struct udphdr *udph = (struct udphdr *)(iph + 1);
|
|
|
|
iph->ip6_vfc = 6;
|
|
iph->ip6_nxt = IPPROTO_UDP;
|
|
iph->ip6_plen = ntohs(sizeof(struct udphdr) + len);
|
|
memcpy(&iph->ip6_src, &((struct sockaddr_in6 *)src)->sin6_addr, sizeof(struct in6_addr));
|
|
memcpy(&iph->ip6_dst, &((struct sockaddr_in6 *)dst)->sin6_addr, sizeof(struct in6_addr));
|
|
|
|
udph->source = ((struct sockaddr_in6 *)src)->sin6_port;
|
|
udph->dest = ((struct sockaddr_in6 *)dst)->sin6_port;
|
|
udph->len = htons(sizeof(struct udphdr) + len);
|
|
|
|
memcpy(udph + 1, buffer, len);
|
|
|
|
udph->check = cksum((uint8_t *)udph, sizeof(struct udphdr) + len);
|
|
|
|
memset(&hdr, 0, sizeof(hdr));
|
|
gettimeofday(&hdr.ts, NULL);
|
|
hdr.caplen = hdr.len = sizeof(struct ip6_hdr) + sizeof(struct udphdr) + len;
|
|
|
|
pcap_dump((u_char *)dumper, &hdr, pkt);
|
|
}
|
|
|
|
}
|
|
|
|
static void remove_wtp(EV_P_ struct wtp *wtp)
|
|
{
|
|
ev_timer_stop (EV_A_ &wtp->timeout);
|
|
ev_io_stop (EV_A_ &wtp->client_ev);
|
|
if (wtp->server.fd)
|
|
close(wtp->server.fd);
|
|
|
|
RB_REMOVE(wtp_tree, &wtp_tree, wtp);
|
|
free(wtp);
|
|
}
|
|
|
|
static void capwap_server_in(EV_P_ struct capwap_port *capwap_port, unsigned char *buffer, ssize_t len, struct sockaddr *addr, socklen_t addrlen);
|
|
static void capwap_fwd(struct wtp *wtp, unsigned char *buffer, ssize_t len);
|
|
|
|
static void capwap_server_cb(EV_P_ ev_io *ev, int revents)
|
|
{
|
|
ssize_t r;
|
|
struct capwap_port *capwap_port = container_of(ev, struct capwap_port, listen_ev);
|
|
struct sockaddr_storage addr;
|
|
socklen_t addrlen = sizeof(addr);
|
|
unsigned char buffer[2048];
|
|
|
|
debug("read (%x) from %d", revents, ev->fd);
|
|
|
|
do {
|
|
r = recvfrom(ev->fd, buffer, sizeof(buffer), MSG_DONTWAIT, (struct sockaddr *)&addr, &addrlen);
|
|
if (r < 0) {
|
|
struct wtp *wtp;
|
|
|
|
if (errno == EAGAIN)
|
|
break;
|
|
else if (errno == EINTR)
|
|
continue;
|
|
|
|
debug("capwap read error: %m");
|
|
if ((wtp = RB_FIND(wtp_tree, &wtp_tree, (struct wtp *)&addr)))
|
|
remove_wtp(EV_A_ wtp);
|
|
|
|
return;
|
|
} else
|
|
capwap_server_in(EV_A_ capwap_port, buffer, r, (struct sockaddr *)&addr, addrlen);
|
|
} while (42);
|
|
}
|
|
|
|
static void wtp_timeout_cb(EV_P_ ev_timer *w, int revents)
|
|
{
|
|
struct wtp *wtp = container_of(w, struct wtp, timeout);
|
|
|
|
debug("got timeout for WTP at %p", wtp);
|
|
remove_wtp(EV_A_ wtp);
|
|
}
|
|
|
|
static void capwap_client_cb(EV_P_ ev_io *ev, int revents)
|
|
{
|
|
ssize_t r;
|
|
struct wtp *wtp = container_of(ev, struct wtp, client_ev);
|
|
unsigned char buffer[2048];
|
|
|
|
debug("read (%x) from %d", revents, ev->fd);
|
|
|
|
do {
|
|
r = recv(ev->fd, buffer, sizeof(buffer), MSG_DONTWAIT);
|
|
if (r < 0) {
|
|
if (errno == EAGAIN)
|
|
break;
|
|
else if (errno == EINTR)
|
|
continue;
|
|
|
|
debug("capwap read error: %m");
|
|
return;
|
|
} else
|
|
capwap_fwd(wtp, buffer, r);
|
|
} while (42);
|
|
|
|
/* reset timeout */
|
|
ev_timer_again(EV_A_ &wtp->timeout);
|
|
}
|
|
|
|
static void adjust_control_ips(unsigned char *buffer, ssize_t len)
|
|
{
|
|
int hlen;
|
|
|
|
if (buffer[0] != 0)
|
|
return;
|
|
|
|
/* plain capwap */
|
|
|
|
/* Adjust CAPWAP Control IPv4 Address in Discover Response messages */
|
|
hlen = ((buffer[1] & 0xf8) >> 3) * 4;
|
|
if (len > hlen) {
|
|
unsigned char *m = buffer + hlen;
|
|
|
|
if (ntohl(*(uint32_t *)m) == 2) { /* discover response */
|
|
int mlen;
|
|
|
|
mlen = ntohs(*(uint16_t *)(m + 5));
|
|
fprintf(stderr, "mlen: %d\n", mlen);
|
|
m += 8;
|
|
|
|
while (mlen > 0) {
|
|
int type;
|
|
int ielen;
|
|
|
|
type = ntohs(*(uint16_t *)m);
|
|
ielen = ntohs(*(uint16_t *)(m + 2)) + 4;
|
|
|
|
if (type == 10 && listen_addr.ss_family == AF_INET) { /* CAPWAP Control IPv4 Address */
|
|
memcpy(m + 4, &((struct sockaddr_in *)&listen_addr)->sin_addr, 4);
|
|
}
|
|
mlen -= ielen;
|
|
m += ielen;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
static void plain_forward(struct dtls_session *recv, struct dtls_session *send_s, unsigned char *buffer, ssize_t len)
|
|
{
|
|
int ret;
|
|
|
|
capwap_dump((struct sockaddr *)&recv->peer_addr, (struct sockaddr *)&send_s->peer_addr, buffer, len);
|
|
adjust_control_ips(buffer, len);
|
|
|
|
if (send_s->is_connected) {
|
|
ret = send(send_s->fd, buffer, len, MSG_DONTWAIT);
|
|
} else
|
|
ret = sendto(send_s->fd, buffer, len, MSG_DONTWAIT, (struct sockaddr *)&send_s->peer_addr, send_s->peer_addrlen);
|
|
|
|
if (ret < 0)
|
|
fprintf(stderr, "%s(%d): %m", !send_s->peer_addrlen ? "send" : "sendto", send_s->fd);
|
|
}
|
|
|
|
static void dtls_forward(struct dtls_session *recv, struct dtls_session *send_s, unsigned char *buffer, ssize_t len)
|
|
{
|
|
int ret;
|
|
|
|
recv->buffer = buffer;
|
|
recv->buffer_len = len;
|
|
|
|
if (!recv->handshake_done) {
|
|
int try_it_again = 1;
|
|
|
|
again:
|
|
do {
|
|
ret = gnutls_handshake(recv->session);
|
|
debug("DTLS handshake on session %p, fd %d, got %d", recv, recv->fd, ret);
|
|
} while (ret == GNUTLS_E_INTERRUPTED);
|
|
|
|
if (ret < 0) {
|
|
if (ret != GNUTLS_E_AGAIN) {
|
|
fprintf(stderr, "Error in handshake(): %s\n", gnutls_strerror(ret));
|
|
gnutls_deinit(recv->session);
|
|
return;
|
|
}
|
|
else if (try_it_again) {
|
|
try_it_again = 0;
|
|
goto again;
|
|
}
|
|
|
|
}
|
|
if (ret == GNUTLS_E_SUCCESS) {
|
|
recv->handshake_done = 1;
|
|
|
|
while (!SIMPLEQ_EMPTY(&recv->plain_q)) {
|
|
struct buffer_q *e = SIMPLEQ_FIRST(&recv->plain_q);
|
|
|
|
debug("dequeue buffer %p on %p", e, recv);
|
|
ret = gnutls_record_send(recv->session, &e->buffer, e->buffer_len);
|
|
debug("GnuTLS send: %d", ret);
|
|
|
|
SIMPLEQ_REMOVE_HEAD(&recv->plain_q, e, entry);
|
|
free(e);
|
|
}
|
|
}
|
|
} else {
|
|
unsigned char sequence[8];
|
|
unsigned char plain_text[MAX_BUFFER];
|
|
|
|
do {
|
|
ret = gnutls_record_recv_seq(recv->session, plain_text, sizeof(plain_text), sequence);
|
|
debug("DTLS record recv on session %p, fd %d, got %d", recv, recv->fd, ret);
|
|
} while (ret == GNUTLS_E_INTERRUPTED);
|
|
|
|
if (ret < 0 && ret != GNUTLS_E_AGAIN) {
|
|
if (gnutls_error_is_fatal(ret) == 0) {
|
|
fprintf(stderr, "*** Warning: %s\n", gnutls_strerror(ret));
|
|
} else {
|
|
fprintf(stderr, "Error in recv(): %s\n", gnutls_strerror(ret));
|
|
gnutls_deinit(recv->session);
|
|
}
|
|
return;
|
|
}
|
|
|
|
debug("received[%.2x%.2x%.2x%.2x%.2x%.2x%.2x%.2x]",
|
|
sequence[0], sequence[1], sequence[2],
|
|
sequence[3], sequence[4], sequence[5],
|
|
sequence[6], sequence[7]);
|
|
// hexdump(plain_text, ret);
|
|
|
|
capwap_dump((struct sockaddr *)&recv->peer_addr, (struct sockaddr *)&send_s->peer_addr, plain_text, ret);
|
|
adjust_control_ips(plain_text, ret);
|
|
|
|
debug("DTLS record send on session %p, fd %d", send_s, send_s->fd);
|
|
if (!send_s->handshake_done) {
|
|
struct buffer_q *e;
|
|
|
|
e = calloc(1, sizeof(struct buffer_q) + ret);
|
|
e->buffer_len = ret;
|
|
memcpy(&e->buffer, plain_text, ret);
|
|
|
|
debug("enqueue buffer %p on %p", e, send_s);
|
|
SIMPLEQ_INSERT_TAIL(&send_s->plain_q, e, entry);
|
|
} else {
|
|
ret = gnutls_record_send(send_s->session, plain_text, ret);
|
|
debug("GnuTLS send: %d", ret);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void capwap_server_in(EV_P_ struct capwap_port *capwap_port, unsigned char *buffer, ssize_t len, struct sockaddr *addr, socklen_t addrlen)
|
|
{
|
|
int ret;
|
|
struct wtp *wtp;
|
|
char ipaddr[INET6_ADDRSTRLEN] __attribute__((unused));
|
|
|
|
#if defined(DEBUG)
|
|
inet_ntop(addr->sa_family, SIN_ADDR_PTR(addr), ipaddr, sizeof(ipaddr));
|
|
debug("on %d (%d) got CAPWAP data from IP: %s:%d, len: %zd\n", capwap_port->listen_fd, capwap_port->port, ipaddr, ntohs(SIN_PORT(addr)), len);
|
|
#endif
|
|
|
|
if (!(wtp = RB_FIND(wtp_tree, &wtp_tree, (struct wtp *)addr))) {
|
|
int on = 1;
|
|
struct sockaddr_storage saddr;
|
|
socklen_t saddr_len = sizeof(saddr);
|
|
|
|
wtp = calloc(1, sizeof(struct wtp));
|
|
if (!wtp)
|
|
return; /* OOM */
|
|
|
|
memcpy(&wtp->addr, addr, addrlen);
|
|
wtp->capwap_port = capwap_port;
|
|
wtp->server.fd = capwap_port->listen_fd;
|
|
wtp->server.peer_addrlen = addrlen;
|
|
memcpy(&wtp->server.peer_addr, addr, addrlen);
|
|
|
|
SIMPLEQ_INIT(&wtp->server.plain_q);
|
|
SIMPLEQ_INIT(&wtp->client.plain_q);
|
|
|
|
ev_init(&wtp->timeout, wtp_timeout_cb);
|
|
wtp->timeout.repeat = 120.;
|
|
|
|
if ((wtp->client.fd = socket(server_addr.ss_family, SOCK_DGRAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0)) < 0) {
|
|
perror("socket");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
setsockopt(wtp->client.fd, SOL_SOCKET, SO_REUSEADDR, (char*)&on, sizeof(on));
|
|
#if !defined(SO_REUSEPORT)
|
|
# warning "SO_REUSEPORT undefined, please upgrade to a newer kernel"
|
|
#else
|
|
setsockopt(wtp->client.fd, SOL_SOCKET, SO_REUSEPORT, (char*)&on, sizeof(on));
|
|
#endif
|
|
setsockopt(wtp->client.fd, SOL_IP, IP_RECVERR, (char*)&on, sizeof(on));
|
|
// setsockopt(wtp->client.fd, SOL_IP, IPV6_V6ONLY,(char*)&on, sizeof(on));
|
|
|
|
on = IP_PMTUDISC_DO;
|
|
setsockopt(wtp->client.fd, SOL_IP, IP_MTU_DISCOVER, (char*)&on, sizeof(on));
|
|
|
|
if (server_addr.ss_family == AF_INET)
|
|
((struct sockaddr_in *)&server_addr)->sin_port = htons(capwap_port->port);
|
|
else
|
|
((struct sockaddr_in6 *)&server_addr)->sin6_port = htons(capwap_port->port);
|
|
|
|
if (connect(wtp->client.fd, (struct sockaddr *)&server_addr, server_addr.ss_family == AF_INET ? sizeof(struct sockaddr_in) : sizeof(struct sockaddr_in6)) < 0) {
|
|
perror("connect");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
wtp->client.is_connected = 1;
|
|
wtp->client.peer_addrlen = sizeof(wtp->client.peer_addr);
|
|
getpeername(wtp->client.fd, (struct sockaddr *)&wtp->client.peer_addr, &wtp->client.peer_addrlen);
|
|
|
|
getsockname(wtp->client.fd, (struct sockaddr *)&saddr, &saddr_len);
|
|
printf("opened CAPWAP server port on %d to %d\n", ntohs(SIN_PORT(&saddr)), capwap_port->port);
|
|
|
|
RB_INSERT(wtp_tree, &wtp_tree, wtp);
|
|
|
|
ev_io_init(&wtp->client_ev, capwap_client_cb, wtp->client.fd, EV_READ);
|
|
ev_io_start(EV_DEFAULT_ &wtp->client_ev);
|
|
}
|
|
|
|
debug("got WTP at %p", wtp);
|
|
|
|
if (buffer[0] == 0) { /* plain capwap */
|
|
plain_forward(&wtp->server, &wtp->client, buffer, len);
|
|
}
|
|
else if (buffer[0] == 0x01) { /* DTLS capwap */
|
|
struct dtls_session *s = &wtp->server;
|
|
struct dtls_session *c = &wtp->client;
|
|
|
|
if (!s->session) {
|
|
gnutls_dtls_prestate_st prestate;
|
|
|
|
memset(&prestate, 0, sizeof(prestate));
|
|
ret = gnutls_dtls_cookie_verify(&cookie_key, (void *)addr, addrlen, (unsigned char*)buffer + 4, len - 4, &prestate);
|
|
|
|
if (ret < 0) { /* cookie not valid */
|
|
gnutls_dtls_cookie_send(&cookie_key, (void *)addr, addrlen, &prestate,
|
|
(gnutls_transport_ptr_t)s, dtls_push_func);
|
|
goto out;
|
|
} else {
|
|
debug("server: %p, client: %p", s, c);
|
|
|
|
gnutls_init(&s->session, GNUTLS_SERVER | GNUTLS_DATAGRAM | GNUTLS_NONBLOCK);
|
|
gnutls_priority_set(s->session, priority_cache);
|
|
gnutls_credentials_set(s->session, GNUTLS_CRD_CERTIFICATE, x509_server_cred);
|
|
|
|
/* prestate is only used to cary to write seq number forward, the buffer will be processed again in dtls_forward ! */
|
|
|
|
gnutls_dtls_prestate_set(s->session, &prestate);
|
|
gnutls_dtls_set_mtu(s->session, 1396);
|
|
|
|
gnutls_transport_set_ptr(s->session, s);
|
|
gnutls_transport_set_push_function(s->session, dtls_push_func);
|
|
gnutls_transport_set_pull_function(s->session, dtls_pull_func);
|
|
gnutls_transport_set_pull_timeout_function(s->session, dtls_pull_timeout_func);
|
|
|
|
gnutls_certificate_server_set_request(s->session, GNUTLS_CERT_REQUEST);
|
|
|
|
gnutls_init(&c->session, GNUTLS_CLIENT | GNUTLS_DATAGRAM | GNUTLS_NONBLOCK);
|
|
gnutls_priority_set(c->session, priority_cache);
|
|
gnutls_credentials_set(c->session, GNUTLS_CRD_CERTIFICATE, x509_client_cred);
|
|
gnutls_dh_set_prime_bits(c->session, 512);
|
|
|
|
gnutls_dtls_set_mtu(c->session, 1396);
|
|
|
|
gnutls_transport_set_ptr(c->session, c);
|
|
gnutls_transport_set_push_function(c->session, dtls_push_func);
|
|
gnutls_transport_set_pull_function(c->session, dtls_pull_func);
|
|
gnutls_transport_set_pull_timeout_function(c->session, dtls_pull_timeout_func);
|
|
|
|
/* Perform the TLS handshake */
|
|
do {
|
|
ret = gnutls_handshake(c->session);
|
|
debug("initial Client DTLS handshake on session %p, fd %d, got %d", c, c->fd, ret);
|
|
}
|
|
while (ret == GNUTLS_E_INTERRUPTED);
|
|
if (ret < 0 && ret != GNUTLS_E_AGAIN) {
|
|
fprintf(stderr, "*** Client Handshake failed\n");
|
|
gnutls_perror(ret);
|
|
goto out;
|
|
}
|
|
}
|
|
}
|
|
|
|
dtls_forward(s, &wtp->client, buffer + 4, len - 4);
|
|
}
|
|
|
|
out:
|
|
/* reset timeout */
|
|
ev_timer_again(EV_A_ &wtp->timeout);
|
|
}
|
|
|
|
static void capwap_fwd(struct wtp *wtp, unsigned char *buffer, ssize_t len)
|
|
{
|
|
if (buffer[0] == 0) { /* plain capwap */
|
|
plain_forward(&wtp->client, &wtp->server, buffer, len);
|
|
}
|
|
else if (buffer[0] == 0x01) /* DTLS capwap */
|
|
dtls_forward(&wtp->client, &wtp->server, buffer + 4, len - 4);
|
|
}
|
|
|
|
/* DTLS functions */
|
|
|
|
static ssize_t dtls_push_func(gnutls_transport_ptr_t p, const void *data, size_t size)
|
|
{
|
|
struct dtls_session *s = p;
|
|
|
|
ssize_t r __attribute__((unused));
|
|
struct iovec iov[2];
|
|
struct msghdr mh;
|
|
char ipaddr[INET6_ADDRSTRLEN] __attribute__((unused));
|
|
|
|
unsigned char preamble[4] = {1, 0, 0, 0};
|
|
|
|
debug("%p: DTLS push of size %zd", s, size);
|
|
|
|
/* The message header contains parameters for sendmsg. */
|
|
memset(&mh, 0, sizeof(mh));
|
|
if (!s->is_connected) {
|
|
|
|
#if defined(DEBUG)
|
|
inet_ntop(s->peer_addr.ss_family, SIN_ADDR_PTR(&s->peer_addr), ipaddr, sizeof(ipaddr));
|
|
debug("DTLS PUSH on %d to IP: %s:%d, len: %zd\n", s->fd, ipaddr, ntohs(SIN_PORT(&s->peer_addr)), size);
|
|
#endif
|
|
|
|
mh.msg_name = (caddr_t)&s->peer_addr;
|
|
mh.msg_namelen = s->peer_addrlen;
|
|
} else
|
|
debug("DTLS PUSH on %d, len: %zd\n", s->fd, size);
|
|
|
|
mh.msg_iov = iov;
|
|
mh.msg_iovlen = 2;
|
|
|
|
iov[0].iov_base = &preamble;
|
|
iov[0].iov_len = sizeof(preamble);
|
|
|
|
iov[1].iov_base = (unsigned char *)data;
|
|
iov[1].iov_len = size;
|
|
|
|
/* FIXME: shortcat write, we do want to use NON-BLOCKING send here and
|
|
* switch to write_ev should it block....
|
|
*/
|
|
if ((r = sendmsg(s->fd, &mh, MSG_DONTWAIT)) < 0) {
|
|
debug("sendmsg on %d: %m", s->fd);
|
|
return r;
|
|
} else {
|
|
debug("sendmsg on %d: %zd", s->fd, r);
|
|
return (r - sizeof(preamble));
|
|
}
|
|
}
|
|
|
|
static ssize_t dtls_pull_func(gnutls_transport_ptr_t p, void *data, size_t size)
|
|
{
|
|
struct dtls_session *s = p;
|
|
|
|
debug("%p: DTLS pull of size %zd", s, size);
|
|
|
|
if (!s->buffer) {
|
|
gnutls_transport_set_errno(s->session, EAGAIN);
|
|
return -1;
|
|
}
|
|
|
|
if (size < s->buffer_len) {
|
|
debug("######################## pull too short: want %zd, have %zd", size, s->buffer_len);
|
|
return -1;
|
|
}
|
|
|
|
memcpy(data, s->buffer, s->buffer_len);
|
|
s->buffer = NULL;
|
|
return s->buffer_len;
|
|
}
|
|
|
|
static int dtls_pull_timeout_func(gnutls_transport_ptr_t p, unsigned int ms)
|
|
{
|
|
struct dtls_session *s = p;
|
|
|
|
debug("%p: DTLS pull timeout", s);
|
|
|
|
if (s->buffer)
|
|
return 1; /* data available */
|
|
|
|
return 0; /* timeout */
|
|
}
|
|
|
|
static int dummy_certificate_verify_function(gnutls_session_t session)
|
|
{
|
|
/* accept anything */
|
|
return 0;
|
|
}
|
|
|
|
static void bind_capwap(struct capwap_port *capwap_port)
|
|
{
|
|
int on = 1;
|
|
|
|
if (listen_addr.ss_family == AF_INET)
|
|
((struct sockaddr_in *)&listen_addr)->sin_port = htons(capwap_port->port);
|
|
else
|
|
((struct sockaddr_in6 *)&listen_addr)->sin6_port = htons(capwap_port->port);
|
|
|
|
if ((capwap_port->listen_fd = socket(listen_addr.ss_family, SOCK_DGRAM | SOCK_NONBLOCK | SOCK_CLOEXEC, 0)) < 0) {
|
|
perror("socket");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
setsockopt(capwap_port->listen_fd, SOL_SOCKET, SO_REUSEADDR, (char*)&on, sizeof(on));
|
|
#if !defined(SO_REUSEPORT)
|
|
# warning "SO_REUSEPORT undefined, please upgrade to a newer kernel"
|
|
#else
|
|
setsockopt(capwap_port->listen_fd, SOL_SOCKET, SO_REUSEPORT, (char*)&on, sizeof(on));
|
|
#endif
|
|
setsockopt(capwap_port->listen_fd, SOL_IP, IP_RECVERR, (char*)&on, sizeof(on));
|
|
// setsockopt(capwap_port->listen_fd, SOL_IP, IPV6_V6ONLY,(char*)&on, sizeof(on));
|
|
|
|
on = IP_PMTUDISC_DO;
|
|
setsockopt(capwap_port->listen_fd, SOL_IP, IP_MTU_DISCOVER, (char*)&on, sizeof(on));
|
|
|
|
if (bind(capwap_port->listen_fd, (struct sockaddr *)&listen_addr, sizeof(listen_addr)) < 0) {
|
|
perror("bind");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
printf("opened CAPWAP listening port %d on fd %d\n", capwap_port->port, capwap_port->listen_fd);
|
|
|
|
ev_io_init(&capwap_port->listen_ev, capwap_server_cb, capwap_port->listen_fd, EV_READ);
|
|
ev_io_start(EV_DEFAULT_ &capwap_port->listen_ev);
|
|
}
|
|
|
|
static void ip_to_addr(const char *ip, struct sockaddr *addr)
|
|
{
|
|
if (strchr(ip, ':') == NULL) {
|
|
addr->sa_family = AF_INET;
|
|
if (inet_pton(AF_INET, ip, &((struct sockaddr_in *)addr)->sin_addr) <= 0) {
|
|
fprintf(stderr, "%s: Not in presentation format\n", ip);
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
} else {
|
|
addr->sa_family = AF_INET6;
|
|
if (inet_pton(AF_INET6, ip, &((struct sockaddr_in6 *)addr)->sin6_addr) <= 0) {
|
|
fprintf(stderr, "%s: Not in presentation format\n", ip);
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
}
|
|
}
|
|
|
|
static void sigint_cb (struct ev_loop *loop, ev_signal *w, int revents)
|
|
{
|
|
ev_break (loop, EVBREAK_ALL);
|
|
}
|
|
|
|
static void usage(void)
|
|
{
|
|
printf("TPLINO CAPWAP MITM Debug Proxy, Version: .....\n\n"
|
|
"Usage: capwap-mitm [OPTION...] LISTEN-IP SERVER-IP \n\n"
|
|
"Options:\n\n"
|
|
" -h this help\n"
|
|
" -p, --port=PORT open CAPWAP MITM proxy on PORT\n"
|
|
" -o FILE write pcap to FILE\n"
|
|
" --server-key=FILE DTLS server certificate key\n"
|
|
" --server-cert=FILE DTLS server certificate\n"
|
|
" --client-key=FILE DTLS client certificate key\n"
|
|
" --client-cert=FILE DTLS client certificate\n"
|
|
" --cafile=FILE DTLS CA chain file\n"
|
|
" --crl=FILE DTLS CRL file\n"
|
|
"\n");
|
|
|
|
exit(EXIT_SUCCESS);
|
|
}
|
|
|
|
#define BLOCK_ALLOC 16
|
|
|
|
int main(int argc, char *argv[])
|
|
{
|
|
const struct rlimit rlim = {
|
|
.rlim_cur = RLIM_INFINITY,
|
|
.rlim_max = RLIM_INFINITY
|
|
};
|
|
|
|
ev_signal signal_watcher;
|
|
|
|
int c;
|
|
int i;
|
|
int capwap_cnt = 0;
|
|
struct capwap_port *capwap_port = NULL;
|
|
char ipaddr[INET6_ADDRSTRLEN] __attribute__((unused));
|
|
|
|
/* unlimited size for cores */
|
|
setrlimit(RLIMIT_CORE, &rlim);
|
|
|
|
while (1) {
|
|
int option_index = 0;
|
|
static struct option long_options[] = {
|
|
{"port", 1, 0, 'p'},
|
|
{"server-key", 1, 0, 1024},
|
|
{"server-cert", 1, 0, 1025},
|
|
{"cafile", 1, 0, 1026},
|
|
{"crl", 1, 0, 1027},
|
|
{"client-key", 1, 0, 1028},
|
|
{"client-cert", 1, 0, 1029},
|
|
{0, 0, 0, 0}
|
|
};
|
|
|
|
c = getopt_long(argc, argv, "h46p:o:",
|
|
long_options, &option_index);
|
|
if (c == -1)
|
|
break;
|
|
|
|
switch (c) {
|
|
case 1024:
|
|
dtls_server_keyfile = strdup(optarg);
|
|
break;
|
|
|
|
case 1025:
|
|
dtls_server_certfile = strdup(optarg);
|
|
break;
|
|
|
|
case 1026:
|
|
dtls_cafile = strdup(optarg);
|
|
break;
|
|
|
|
case 1027:
|
|
dtls_crlfile = strdup(optarg);
|
|
break;
|
|
|
|
case 1028:
|
|
dtls_client_keyfile = strdup(optarg);
|
|
break;
|
|
|
|
case 1029:
|
|
dtls_client_certfile = strdup(optarg);
|
|
break;
|
|
|
|
case 'h':
|
|
usage();
|
|
break;
|
|
|
|
case 'p':
|
|
if (capwap_cnt % BLOCK_ALLOC == 0) {
|
|
capwap_port = realloc(capwap_port, sizeof(struct capwap_port) * (capwap_cnt + BLOCK_ALLOC));
|
|
memset(&capwap_port[capwap_cnt], 0, sizeof(struct capwap_port) * BLOCK_ALLOC);
|
|
}
|
|
capwap_port[capwap_cnt].port = strtol(optarg, NULL, 0);
|
|
if (errno != 0) {
|
|
fprintf(stderr, "Invalid numeric argument: '%s'\n", optarg);
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
capwap_cnt++;
|
|
break;
|
|
|
|
case 'o':
|
|
pcap_fname = strdup(optarg);
|
|
break;
|
|
|
|
default:
|
|
printf("?? getopt returned character code 0%o ??\n", c);
|
|
}
|
|
}
|
|
|
|
if (optind != argc - 2) {
|
|
fprintf(stderr, "Expected argument after options\n");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
printf("Listen = %s, Server = %s\n", argv[optind], argv[optind + 1]);
|
|
ip_to_addr(argv[optind], (struct sockaddr *)&listen_addr);
|
|
ip_to_addr(argv[optind + 1], (struct sockaddr *)&server_addr);
|
|
|
|
#if defined(DEBUG)
|
|
inet_ntop(server_addr.ss_family, SIN_ADDR_PTR(&server_addr), ipaddr, sizeof(ipaddr));
|
|
debug("CAPWAP server on: %s\n", ipaddr);
|
|
#endif
|
|
|
|
/* this must be called once in the program */
|
|
gnutls_global_init();
|
|
|
|
if (access(dtls_server_keyfile, R_OK) < 0)
|
|
dtls_server_keyfile = dtls_server_certfile;
|
|
|
|
gnutls_certificate_allocate_credentials(&x509_server_cred);
|
|
gnutls_certificate_set_x509_trust_file(x509_server_cred, dtls_cafile, GNUTLS_X509_FMT_PEM);
|
|
gnutls_certificate_set_x509_crl_file(x509_server_cred, dtls_crlfile, GNUTLS_X509_FMT_PEM);
|
|
|
|
if (gnutls_certificate_set_x509_key_file(x509_server_cred, dtls_server_certfile, dtls_server_keyfile, GNUTLS_X509_FMT_PEM) < 0) {
|
|
printf("No server certificate or key were found\n");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
gnutls_certificate_set_verify_function(x509_server_cred, dummy_certificate_verify_function);
|
|
|
|
if (access(dtls_client_keyfile, R_OK) < 0)
|
|
dtls_client_keyfile = dtls_client_certfile;
|
|
|
|
gnutls_certificate_allocate_credentials(&x509_client_cred);
|
|
gnutls_certificate_set_x509_trust_file(x509_client_cred, dtls_cafile, GNUTLS_X509_FMT_PEM);
|
|
gnutls_certificate_set_x509_crl_file(x509_client_cred, dtls_crlfile, GNUTLS_X509_FMT_PEM);
|
|
|
|
if (gnutls_certificate_set_x509_key_file(x509_client_cred, dtls_client_certfile, dtls_client_keyfile, GNUTLS_X509_FMT_PEM) < 0) {
|
|
printf("No client certificate or key were found\n");
|
|
exit(EXIT_FAILURE);
|
|
}
|
|
|
|
int bits = gnutls_sec_param_to_pk_bits(GNUTLS_PK_DH, GNUTLS_SEC_PARAM_INSECURE);
|
|
/* Generate Diffie-Hellman parameters - for use with DHE
|
|
* kx algorithms. When short bit length is used, it might
|
|
* be wise to regenerate parameters often.
|
|
*/
|
|
gnutls_dh_params_init(&dh_client_params);
|
|
gnutls_dh_params_generate2(dh_client_params, bits);
|
|
gnutls_certificate_set_dh_params(x509_client_cred, dh_client_params);
|
|
|
|
bits = gnutls_sec_param_to_pk_bits(GNUTLS_PK_DH, GNUTLS_SEC_PARAM_LEGACY);
|
|
gnutls_dh_params_init(&dh_server_params);
|
|
gnutls_dh_params_generate2(dh_server_params, bits);
|
|
gnutls_certificate_set_dh_params(x509_server_cred, dh_server_params);
|
|
|
|
|
|
#if GNUTLS_VERSION_NUMBER > 0x0302ff
|
|
gnutls_priority_init(&priority_cache,
|
|
"LEGACY:-VERS-TLS-ALL:+VERS-DTLS1.0:%SERVER_PRECEDENCE", NULL);
|
|
#else
|
|
gnutls_priority_init(&priority_cache,
|
|
"NORMAL:-VERS-TLS-ALL:+VERS-DTLS1.0:%SERVER_PRECEDENCE", NULL);
|
|
#endif
|
|
gnutls_key_generate(&cookie_key, GNUTLS_COOKIE_KEY_SIZE);
|
|
|
|
|
|
if (pcap_fname) {
|
|
pcap = pcap_open_dead(DLT_RAW, 65535);
|
|
if (strcmp(pcap_fname, "-") == 0)
|
|
dumper = pcap_dump_fopen(pcap, stdout);
|
|
else
|
|
dumper = pcap_dump_open(pcap, pcap_fname);
|
|
}
|
|
|
|
if (capwap_cnt == 0) {
|
|
capwap_port = calloc(BLOCK_ALLOC, sizeof(struct capwap_port));
|
|
capwap_port[0].port = 5246;
|
|
capwap_port[1].port = 5247;
|
|
capwap_cnt = 2;
|
|
}
|
|
|
|
for (i = 0; i < capwap_cnt; i++)
|
|
bind_capwap(&capwap_port[i]);
|
|
|
|
ev_signal_init(&signal_watcher, sigint_cb, SIGINT);
|
|
ev_signal_start(EV_DEFAULT_ &signal_watcher);
|
|
|
|
ev_run(EV_DEFAULT_ 0);
|
|
|
|
if (dumper)
|
|
pcap_dump_close(dumper);
|
|
|
|
return 0;
|
|
}
|