[Openvpn-devel,4/4] transport-plugin: add sample obfs-test plugin

Message ID 20181230112901.29241-5-a@unstable.cc
State New
Headers show
Series
  • Transport API: offload traffic manipulation to plugins
Related show

Commit Message

Antonio Quartulli Dec. 30, 2018, 11:29 a.m.
From: Robin Tarsiger <rtt@dasyatidae.com>

Add a sample plugin to explain how the new transport API is expected to
be implemented and work. It can be used for testing.

Signed-off-by: Robin Tarsiger <rtt@dasyatidae.com>
[antonio@openvpn.net: refactored commits, restyled code]
---
 configure.ac                              |   9 +
 src/plugins/Makefile.am                   |   2 +-
 src/plugins/obfs-test/Makefile.am         |  29 ++
 src/plugins/obfs-test/README.obfs-test    |  26 +
 src/plugins/obfs-test/obfs-test-args.c    |  60 +++
 src/plugins/obfs-test/obfs-test-munging.c | 129 +++++
 src/plugins/obfs-test/obfs-test-posix.c   | 207 ++++++++
 src/plugins/obfs-test/obfs-test-win32.c   | 579 ++++++++++++++++++++++
 src/plugins/obfs-test/obfs-test.c         |  94 ++++
 src/plugins/obfs-test/obfs-test.exports   |   4 +
 src/plugins/obfs-test/obfs-test.h         |  42 ++
 11 files changed, 1180 insertions(+), 1 deletion(-)
 create mode 100644 src/plugins/obfs-test/Makefile.am
 create mode 100644 src/plugins/obfs-test/README.obfs-test
 create mode 100644 src/plugins/obfs-test/obfs-test-args.c
 create mode 100644 src/plugins/obfs-test/obfs-test-munging.c
 create mode 100644 src/plugins/obfs-test/obfs-test-posix.c
 create mode 100644 src/plugins/obfs-test/obfs-test-win32.c
 create mode 100644 src/plugins/obfs-test/obfs-test.c
 create mode 100644 src/plugins/obfs-test/obfs-test.exports
 create mode 100644 src/plugins/obfs-test/obfs-test.h

Comments

Justin Henck via Openvpn-devel Jan. 28, 2019, 6:11 p.m. | #1
Bumping this as well given the holiday hiatus - it seems like there was
feedback on the patches 2 & 3. Does anyone have any feedback for this one?

Thanks!
Justin

Justin Henck
Product Manager
212-565-9811
google.com/jigsaw

PGP: EA8E 8C27 2D75 974D B357 482B 1039 9F2D 869A 117B


On Sun, Dec 30, 2018 at 6:31 AM Antonio Quartulli <a@unstable.cc> wrote:

> From: Robin Tarsiger <rtt@dasyatidae.com>
>
> Add a sample plugin to explain how the new transport API is expected to
> be implemented and work. It can be used for testing.
>
> Signed-off-by: Robin Tarsiger <rtt@dasyatidae.com>
> [antonio@openvpn.net: refactored commits, restyled code]
> ---
>  configure.ac                              |   9 +
>  src/plugins/Makefile.am                   |   2 +-
>  src/plugins/obfs-test/Makefile.am         |  29 ++
>  src/plugins/obfs-test/README.obfs-test    |  26 +
>  src/plugins/obfs-test/obfs-test-args.c    |  60 +++
>  src/plugins/obfs-test/obfs-test-munging.c | 129 +++++
>  src/plugins/obfs-test/obfs-test-posix.c   | 207 ++++++++
>  src/plugins/obfs-test/obfs-test-win32.c   | 579 ++++++++++++++++++++++
>  src/plugins/obfs-test/obfs-test.c         |  94 ++++
>  src/plugins/obfs-test/obfs-test.exports   |   4 +
>  src/plugins/obfs-test/obfs-test.h         |  42 ++
>  11 files changed, 1180 insertions(+), 1 deletion(-)
>  create mode 100644 src/plugins/obfs-test/Makefile.am
>  create mode 100644 src/plugins/obfs-test/README.obfs-test
>  create mode 100644 src/plugins/obfs-test/obfs-test-args.c
>  create mode 100644 src/plugins/obfs-test/obfs-test-munging.c
>  create mode 100644 src/plugins/obfs-test/obfs-test-posix.c
>  create mode 100644 src/plugins/obfs-test/obfs-test-win32.c
>  create mode 100644 src/plugins/obfs-test/obfs-test.c
>  create mode 100644 src/plugins/obfs-test/obfs-test.exports
>  create mode 100644 src/plugins/obfs-test/obfs-test.h
>
> diff --git a/configure.ac b/configure.ac
> index 1e6891b1..b4196812 100644
> --- a/configure.ac
> +++ b/configure.ac
> @@ -200,6 +200,13 @@ AC_ARG_ENABLE(
>         ]
>  )
>
> +AC_ARG_ENABLE(
> +       [plugin-obfs-test],
> +       [AS_HELP_STRING([--disable-plugin-obfs-test], [disable obfs-test
> plugin @<:@default=platform specific@:>@])],
> +       ,
> +       [enable_plugin_obfs_test="no"]
> +)
> +
>  AC_ARG_ENABLE(
>         [pam-dlopen],
>         [AS_HELP_STRING([--enable-pam-dlopen], [dlopen libpam
> @<:@default=no@:>@])],
> @@ -1344,6 +1351,7 @@ AM_CONDITIONAL([WIN32], [test "${WIN32}" = "yes"])
>  AM_CONDITIONAL([GIT_CHECKOUT], [test "${GIT_CHECKOUT}" = "yes"])
>  AM_CONDITIONAL([ENABLE_PLUGIN_AUTH_PAM], [test
> "${enable_plugin_auth_pam}" = "yes"])
>  AM_CONDITIONAL([ENABLE_PLUGIN_DOWN_ROOT], [test
> "${enable_plugin_down_root}" = "yes"])
> +AM_CONDITIONAL([ENABLE_PLUGIN_OBFS_TEST], [test
> "${enable_plugin_obfs_test}" = "yes"])
>  AM_CONDITIONAL([HAVE_LD_WRAP_SUPPORT], [test "${have_ld_wrap_support}" =
> "yes"])
>
>  sampledir="\$(docdir)/sample"
> @@ -1403,6 +1411,7 @@ AC_CONFIG_FILES([
>         src/plugins/Makefile
>         src/plugins/auth-pam/Makefile
>         src/plugins/down-root/Makefile
> +       src/plugins/obfs-test/Makefile
>         tests/Makefile
>          tests/unit_tests/Makefile
>          tests/unit_tests/example_test/Makefile
> diff --git a/src/plugins/Makefile.am b/src/plugins/Makefile.am
> index f3461786..848bac03 100644
> --- a/src/plugins/Makefile.am
> +++ b/src/plugins/Makefile.am
> @@ -12,4 +12,4 @@
>  MAINTAINERCLEANFILES = \
>         $(srcdir)/Makefile.in
>
> -SUBDIRS = auth-pam down-root
> +SUBDIRS = auth-pam down-root obfs-test
> diff --git a/src/plugins/obfs-test/Makefile.am
> b/src/plugins/obfs-test/Makefile.am
> new file mode 100644
> index 00000000..4cc8d183
> --- /dev/null
> +++ b/src/plugins/obfs-test/Makefile.am
> @@ -0,0 +1,29 @@
> +MAINTAINERCLEANFILES = \
> +       $(srcdir)/Makefile.in
> +
> +AM_CFLAGS = \
> +       -I$(top_srcdir)/include \
> +       $(OPTIONAL_CRYPTO_CFLAGS)
> +
> +if ENABLE_PLUGIN_OBFS_TEST
> +plugin_LTLIBRARIES = openvpn-plugin-obfs-test.la
> +endif
> +
> +openvpn_plugin_obfs_test_la_SOURCES = \
> +       obfs-test.c \
> +       obfs-test-munging.c \
> +       obfs-test-args.c \
> +       obfs-test.exports
> +
> +if WIN32
> +openvpn_plugin_obfs_test_la_SOURCES += obfs-test-win32.c
> +openvpn_plugin_obfs_test_la_LIBADD = -lws2_32 -lwininet
> +else !WIN32
> +openvpn_plugin_obfs_test_la_SOURCES += obfs-test-posix.c
> +# No LIBADD necessary; we assume we can access the global symbol space,
> +# and core OpenVPN will already link with everything needed for sockets.
> +endif
> +
> +openvpn_plugin_obfs_test_la_LDFLAGS = $(AM_LDFLAGS) \
> +       -export-symbols "$(srcdir)/obfs-test.exports" \
> +       -module -shared -avoid-version -no-undefined
> diff --git a/src/plugins/obfs-test/README.obfs-test
> b/src/plugins/obfs-test/README.obfs-test
> new file mode 100644
> index 00000000..5492ee02
> --- /dev/null
> +++ b/src/plugins/obfs-test/README.obfs-test
> @@ -0,0 +1,26 @@
> +obfs-test
> +
> +SYNOPSIS
> +
> +The obfs-test plugin is a proof of concept for supporting protocol
> +obfuscation for OpenVPN via a socket intercept plugin.
> +
> +BUILD
> +
> +You must specify --enable-plugin-obfs-test at configure time to
> +trigger building this plugin. It should function on POSIX-y platforms
> +and Windows.
> +
> +USAGE
> +
> +To invoke this plugin, load it via an appropriate plugin line in the
> +configuration file, and then specify 'proto indirect' rather than any
> +other protocol. Packets will then be passed via UDP, but they will
> +also undergo a very basic content transformation, and the bind port
> +will be altered (see obfs-test-munging.c for details).
> +
> +CAVEATS
> +
> +This has undergone basic functionality testing, but not any kind of
> +full-on stress test. Extended socket or I/O handling options are not
> +supported at all.
> diff --git a/src/plugins/obfs-test/obfs-test-args.c
> b/src/plugins/obfs-test/obfs-test-args.c
> new file mode 100644
> index 00000000..e6756f8f
> --- /dev/null
> +++ b/src/plugins/obfs-test/obfs-test-args.c
> @@ -0,0 +1,60 @@
> +#include "obfs-test.h"
> +
> +openvpn_transport_args_t
> +obfs_test_parseargs(void *plugin_handle,
> +                    const char *const *argv, int argc)
> +{
> +    struct obfs_test_args *args = calloc(1, sizeof(struct
> obfs_test_args));
> +    if (!args)
> +    {
> +        return NULL;
> +    }
> +
> +    if (argc < 2)
> +    {
> +        args->offset = 0;
> +    }
> +    else if (argc == 2)
> +    {
> +        char *end;
> +        long offset = strtol(argv[1], &end, 10);
> +        if (*end != '\0')
> +        {
> +            args->error = "offset must be a decimal number";
> +        }
> +        else if (!(0 <= offset && offset <= 42))
> +        {
> +            args->error = "offset must be between 0 and 42";
> +        }
> +        else
> +        {
> +            args->offset = (int) offset;
> +        }
> +    }
> +    else
> +    {
> +        args->error = "too many arguments";
> +    }
> +
> +    return args;
> +}
> +
> +const char *
> +obfs_test_argerror(openvpn_transport_args_t args_)
> +{
> +    if (!args_)
> +    {
> +        return "cannot allocate";
> +    }
> +    else
> +    {
> +        return ((struct obfs_test_args *) args_)->error;
> +    }
> +}
> +
> +void
> +obfs_test_freeargs(openvpn_transport_args_t args_)
> +{
> +    free(args_);
> +    struct obfs_test_args *args = (struct obfs_test_args *) args_;
> +}
> diff --git a/src/plugins/obfs-test/obfs-test-munging.c
> b/src/plugins/obfs-test/obfs-test-munging.c
> new file mode 100644
> index 00000000..37d27039
> --- /dev/null
> +++ b/src/plugins/obfs-test/obfs-test-munging.c
> @@ -0,0 +1,129 @@
> +#include <string.h>
> +#include <errno.h>
> +#include <stdbool.h>
> +#include "obfs-test.h"
> +#ifdef OPENVPN_TRANSPORT_PLATFORM_POSIX
> +#include <sys/socket.h>
> +#include <netinet/in.h>
> +typedef in_port_t obfs_test_in_port_t;
> +#else
> +#include <winsock2.h>
> +#include <ws2tcpip.h>
> +typedef u_short obfs_test_in_port_t;
> +#endif
> +
> +static obfs_test_in_port_t
> +munge_port(obfs_test_in_port_t port)
> +{
> +    return port ^ 15;
> +}
> +
> +/* Reversible. */
> +void
> +obfs_test_munge_addr(struct sockaddr *addr, openvpn_transport_socklen_t
> len)
> +{
> +    struct sockaddr_in *inet;
> +    struct sockaddr_in6 *inet6;
> +
> +    switch (addr->sa_family)
> +    {
> +        case AF_INET:
> +            inet = (struct sockaddr_in *) addr;
> +            inet->sin_port = munge_port(inet->sin_port);
> +            break;
> +
> +        case AF_INET6:
> +            inet6 = (struct sockaddr_in6 *) addr;
> +            inet6->sin6_port = munge_port(inet6->sin6_port);
> +            break;
> +
> +        default:
> +            break;
> +    }
> +}
> +
> +/* Six fixed bytes, six repeated bytes. It's only a silly transformation.
> */
> +#define MUNGE_OVERHEAD 12
> +
> +size_t
> +obfs_test_max_munged_buf_size(size_t clear_size)
> +{
> +    return clear_size + MUNGE_OVERHEAD;
> +}
> +
> +ssize_t
> +obfs_test_unmunge_buf(struct obfs_test_args *how,
> +                      char *buf, size_t len)
> +{
> +    int i;
> +
> +    if (len < 6)
> +    {
> +        goto bad;
> +    }
> +    for (i = 0; i < 6; i++)
> +    {
> +        if (buf[i] != i + how->offset)
> +        {
> +            goto bad;
> +        }
> +    }
> +
> +    for (i = 0; i < 6 && (6 + 2*i) < len; i++)
> +    {
> +        if (len < (6 + 2*i + 1) || buf[6 + 2*i] != buf[6 + 2*i + 1])
> +        {
> +            goto bad;
> +        }
> +        buf[i] = buf[6 + 2*i];
> +    }
> +
> +    if (len > 18)
> +    {
> +        memmove(buf + 6, buf + 18, len - 18);
> +        len -= 12;
> +    }
> +    else
> +    {
> +        len -= 6;
> +        len /= 2;
> +    }
> +
> +    return len;
> +
> +bad:
> +    /* TODO: this really isn't the best way to report this error */
> +    errno = EIO;
> +    return -1;
> +}
> +
> +/* out must have space for len+MUNGE_OVERHEAD bytes. out and in must
> + * not overlap. */
> +size_t
> +obfs_test_munge_buf(struct obfs_test_args *how,
> +                    char *out, const char *in, size_t len)
> +{
> +    int i, n;
> +    size_t out_len = 6;
> +
> +    for (i = 0; i < 6; i++)
> +    {
> +        out[i] = i + how->offset;
> +    }
> +    n = len < 6 ? len : 6;
> +    for (i = 0; i < n; i++)
> +    {
> +        out[6 + 2*i] = out[6 + 2*i + 1] = in[i];
> +    }
> +    if (len > 6)
> +    {
> +        memmove(out + 18, in + 6, len - 6);
> +        out_len = len + 12;
> +    }
> +    else
> +    {
> +        out_len = 6 + 2*len;
> +    }
> +
> +    return out_len;
> +}
> diff --git a/src/plugins/obfs-test/obfs-test-posix.c
> b/src/plugins/obfs-test/obfs-test-posix.c
> new file mode 100644
> index 00000000..826381c5
> --- /dev/null
> +++ b/src/plugins/obfs-test/obfs-test-posix.c
> @@ -0,0 +1,207 @@
> +#include "obfs-test.h"
> +#include <stdbool.h>
> +#include <string.h>
> +#include <err.h>
> +#include <errno.h>
> +#include <unistd.h>
> +#include <fcntl.h>
> +#include <sys/socket.h>
> +#include <netinet/in.h>
> +
> +struct obfs_test_socket_posix
> +{
> +    struct openvpn_transport_socket handle;
> +    struct obfs_test_args args;
> +    struct obfs_test_context *ctx;
> +    int fd;
> +    unsigned last_rwflags;
> +};
> +
> +static void
> +free_socket(struct obfs_test_socket_posix *sock)
> +{
> +    if (!sock)
> +    {
> +        return;
> +    }
> +    if (sock->fd != -1)
> +    {
> +        close(sock->fd);
> +    }
> +    free(sock);
> +}
> +
> +static openvpn_transport_socket_t
> +obfs_test_posix_bind(void *plugin_handle, openvpn_transport_args_t args,
> +                     const struct sockaddr *addr, socklen_t len)
> +{
> +    struct obfs_test_socket_posix *sock = NULL;
> +    struct sockaddr *addr_rev = NULL;
> +
> +    addr_rev = calloc(1, len);
> +    if (!addr_rev)
> +    {
> +        goto error;
> +    }
> +    memcpy(addr_rev, addr, len);
> +    obfs_test_munge_addr(addr_rev, len);
> +
> +    sock = calloc(1, sizeof(struct obfs_test_socket_posix));
> +    if (!sock)
> +    {
> +        goto error;
> +    }
> +    sock->handle.vtab = &obfs_test_socket_vtab;
> +    sock->ctx = (struct obfs_test_context *) plugin_handle;
> +    memcpy(&sock->args, args, sizeof(sock->args));
> +    /* Note that sock->fd isn't -1 yet. Set it explicitly if there are
> ever any
> +     * error exits before the socket() call. */
> +
> +    sock->fd = socket(addr->sa_family, SOCK_DGRAM, IPPROTO_UDP);
> +    if (sock->fd == -1)
> +    {
> +        goto error;
> +    }
> +    if (fcntl(sock->fd, F_SETFL, fcntl(sock->fd, F_GETFL) | O_NONBLOCK))
> +    {
> +        goto error;
> +    }
> +
> +    if (bind(sock->fd, addr_rev, len))
> +    {
> +        goto error;
> +    }
> +    free(addr_rev);
> +    return &sock->handle;
> +
> +error:
> +    free_socket(sock);
> +    free(addr_rev);
> +    return NULL;
> +}
> +
> +static void
> +obfs_test_posix_request_event(openvpn_transport_socket_t handle,
> +                              openvpn_transport_event_set_handle_t
> event_set, unsigned rwflags)
> +{
> +    obfs_test_log(((struct obfs_test_socket_posix *) handle)->ctx,
> +                  PLOG_DEBUG, "request-event: %d", rwflags);
> +    ((struct obfs_test_socket_posix *) handle)->last_rwflags = 0;
> +    if (rwflags)
> +    {
> +        event_set->vtab->set_event(event_set, ((struct
> obfs_test_socket_posix *) handle)->fd,
> +                                   rwflags, handle);
> +    }
> +}
> +
> +static bool
> +obfs_test_posix_update_event(openvpn_transport_socket_t handle, void
> *arg, unsigned rwflags)
> +{
> +    obfs_test_log(((struct obfs_test_socket_posix *) handle)->ctx,
> +                  PLOG_DEBUG, "update-event: %p, %p, %d", handle, arg,
> rwflags);
> +    if (arg != handle)
> +    {
> +        return false;
> +    }
> +    ((struct obfs_test_socket_posix *) handle)->last_rwflags |= rwflags;
> +    return true;
> +}
> +
> +static unsigned
> +obfs_test_posix_pump(openvpn_transport_socket_t handle)
> +{
> +    obfs_test_log(((struct obfs_test_socket_posix *) handle)->ctx,
> +                  PLOG_DEBUG, "pump -> %d", ((struct
> obfs_test_socket_posix *) handle)->last_rwflags);
> +    return ((struct obfs_test_socket_posix *) handle)->last_rwflags;
> +}
> +
> +static ssize_t
> +obfs_test_posix_recvfrom(openvpn_transport_socket_t handle, void *buf,
> size_t len,
> +                         struct sockaddr *addr, socklen_t *addrlen)
> +{
> +    int fd = ((struct obfs_test_socket_posix *) handle)->fd;
> +    ssize_t result;
> +
> +again:
> +    result = recvfrom(fd, buf, len, 0, addr, addrlen);
> +    if (result < 0 && errno == EAGAIN)
> +    {
> +        ((struct obfs_test_socket_posix *) handle)->last_rwflags &=
> ~OPENVPN_TRANSPORT_EVENT_READ;
> +    }
> +    if (*addrlen > 0)
> +    {
> +        obfs_test_munge_addr(addr, *addrlen);
> +    }
> +    if (result > 0)
> +    {
> +        struct obfs_test_args *how = &((struct obfs_test_socket_posix *)
> handle)->args;
> +        result = obfs_test_unmunge_buf(how, buf, result);
> +        if (result < 0)
> +        {
> +            /* Pretend that read never happened. */
> +            goto again;
> +        }
> +    }
> +
> +    obfs_test_log(((struct obfs_test_socket_posix *) handle)->ctx,
> +                  PLOG_DEBUG, "recvfrom(%d) -> %d", (int)len,
> (int)result);
> +    return result;
> +}
> +
> +static ssize_t
> +obfs_test_posix_sendto(openvpn_transport_socket_t handle, const void
> *buf, size_t len,
> +                       const struct sockaddr *addr, socklen_t addrlen)
> +{
> +    int fd = ((struct obfs_test_socket_posix *) handle)->fd;
> +    struct sockaddr *addr_rev = calloc(1, addrlen);
> +    void *buf_munged = malloc(obfs_test_max_munged_buf_size(len));
> +    size_t len_munged;
> +    ssize_t result;
> +    if (!addr_rev || !buf_munged)
> +    {
> +        goto error;
> +    }
> +
> +    memcpy(addr_rev, addr, addrlen);
> +    obfs_test_munge_addr(addr_rev, addrlen);
> +    struct obfs_test_args *how = &((struct obfs_test_socket_posix *)
> handle)->args;
> +    len_munged = obfs_test_munge_buf(how, buf_munged, buf, len);
> +    result = sendto(fd, buf_munged, len_munged, 0, addr_rev, addrlen);
> +    if (result < 0 && errno == EAGAIN)
> +    {
> +        ((struct obfs_test_socket_posix *) handle)->last_rwflags &=
> ~OPENVPN_TRANSPORT_EVENT_WRITE;
> +    }
> +    /* TODO: not clear what to do here for partial transfers. */
> +    if (result > len)
> +    {
> +        result = len;
> +    }
> +    obfs_test_log(((struct obfs_test_socket_posix *) handle)->ctx,
> +                  PLOG_DEBUG, "sendto(%d) -> %d", (int)len, (int)result);
> +    free(addr_rev);
> +    free(buf_munged);
> +    return result;
> +
> +error:
> +    free(addr_rev);
> +    free(buf_munged);
> +    return -1;
> +}
> +
> +static void
> +obfs_test_posix_close(openvpn_transport_socket_t handle)
> +{
> +    free_socket((struct obfs_test_socket_posix *) handle);
> +}
> +
> +void
> +obfs_test_initialize_vtabs_platform(void)
> +{
> +    obfs_test_bind_vtab.bind = obfs_test_posix_bind;
> +    obfs_test_socket_vtab.request_event = obfs_test_posix_request_event;
> +    obfs_test_socket_vtab.update_event = obfs_test_posix_update_event;
> +    obfs_test_socket_vtab.pump = obfs_test_posix_pump;
> +    obfs_test_socket_vtab.recvfrom = obfs_test_posix_recvfrom;
> +    obfs_test_socket_vtab.sendto = obfs_test_posix_sendto;
> +    obfs_test_socket_vtab.close = obfs_test_posix_close;
> +}
> diff --git a/src/plugins/obfs-test/obfs-test-win32.c
> b/src/plugins/obfs-test/obfs-test-win32.c
> new file mode 100644
> index 00000000..46c95f55
> --- /dev/null
> +++ b/src/plugins/obfs-test/obfs-test-win32.c
> @@ -0,0 +1,579 @@
> +#include "obfs-test.h"
> +#include <stdbool.h>
> +#include <string.h>
> +#include <stdio.h>
> +#include <stdarg.h>
> +#include <windows.h>
> +#include <winsock2.h>
> +#include <assert.h>
> +
> +static inline bool
> +is_invalid_handle(HANDLE h)
> +{
> +    return h == NULL || h == INVALID_HANDLE_VALUE;
> +}
> +
> +typedef enum {
> +    IO_SLOT_DORMANT,            /* must be 0 for calloc purposes */
> +    IO_SLOT_PENDING,
> +    /* success/failure is determined by succeeded flag in COMPLETE state
> */
> +    IO_SLOT_COMPLETE
> +} io_slot_status_t;
> +
> +/* must be calloc'able */
> +struct io_slot
> +{
> +    struct obfs_test_context *ctx;
> +    io_slot_status_t status;
> +    OVERLAPPED overlapped;
> +    SOCKET socket;
> +    SOCKADDR_STORAGE addr;
> +    int addr_len, addr_cap;
> +    DWORD bytes, flags;
> +    bool succeeded;
> +    int wsa_error;
> +
> +    /* realloc'd as needed; always private copy, never aliased */
> +    char *buf;
> +    size_t buf_len, buf_cap;
> +};
> +
> +static bool
> +setup_io_slot(struct io_slot *slot, struct obfs_test_context *ctx,
> +              SOCKET socket, HANDLE event)
> +{
> +    slot->ctx = ctx;
> +    slot->status = IO_SLOT_DORMANT;
> +    slot->addr_cap = sizeof(SOCKADDR_STORAGE);
> +    slot->socket = socket;
> +    slot->overlapped.hEvent = event;
> +    return true;
> +}
> +
> +/* Note that this assumes any I/O has already been implicitly canceled
> (via closesocket),
> + * but not waited for yet. */
> +static bool
> +destroy_io_slot(struct io_slot *slot)
> +{
> +    if (slot->status == IO_SLOT_PENDING)
> +    {
> +        DWORD bytes, flags;
> +        BOOL ok = WSAGetOverlappedResult(slot->socket, &slot->overlapped,
> &bytes,
> +                                         TRUE /* wait */, &flags);
> +        if (!ok && WSAGetLastError() == WSA_IO_INCOMPLETE)
> +        {
> +            obfs_test_log(slot->ctx, PLOG_ERR,
> +                          "destroying I/O slot: canceled operation is
> still incomplete after wait?!");
> +            return false;
> +        }
> +    }
> +
> +    slot->status = IO_SLOT_DORMANT;
> +    return true;
> +}
> +
> +/* FIXME: aborts on error. */
> +static void
> +resize_io_buf(struct io_slot *slot, size_t cap)
> +{
> +    if (slot->buf)
> +    {
> +        free(slot->buf);
> +        slot->buf = NULL;
> +    }
> +
> +    char *new_buf = malloc(cap);
> +    if (!new_buf)
> +    {
> +        abort();
> +    }
> +    slot->buf = new_buf;
> +    slot->buf_cap = cap;
> +}
> +
> +struct obfs_test_socket_win32
> +{
> +    struct openvpn_transport_socket handle;
> +    struct obfs_test_args args;
> +    struct obfs_test_context *ctx;
> +    SOCKET socket;
> +
> +    /* Write is ready when idle; read is not-ready when idle. Both
> level-triggered. */
> +    struct openvpn_transport_win32_event_pair completion_events;
> +    struct io_slot slot_read, slot_write;
> +
> +    int last_rwflags;
> +};
> +
> +static void
> +free_socket(struct obfs_test_socket_win32 *sock)
> +{
> +    /* This only ever becomes false in strange situations where we leak
> the entire structure for
> +     * lack of anything else to do. */
> +    bool can_free = true;
> +
> +    if (!sock)
> +    {
> +        return;
> +    }
> +    if (sock->socket != INVALID_SOCKET)
> +    {
> +        closesocket(sock->socket);
> +    }
> +
> +    /* closesocket cancels any pending overlapped I/O, but we still have
> to potentially
> +     * wait for it here before we can free the buffers. This has to
> happen before closing
> +     * the event handles.
> +     *
> +     * If we can't figure out when the canceled overlapped I/O is done,
> for any reason, we defensively
> +     * leak the entire structure; freeing it would be permitting the
> system to corrupt memory later.
> +     * TODO: possibly abort() instead, but make sure we've handled all
> the possible "have to try again"
> +     * cases above first
> +     */
> +    if (!destroy_io_slot(&sock->slot_read))
> +    {
> +        can_free = false;
> +    }
> +    if (!destroy_io_slot(&sock->slot_write))
> +    {
> +        can_free = false;
> +    }
> +    if (!can_free)
> +    {
> +        /* Skip deinitialization of everything else. Doomed. */
> +        obfs_test_log(sock->ctx, PLOG_ERR, "doomed, leaking the entire
> socket structure");
> +        return;
> +    }
> +
> +    if (!is_invalid_handle(sock->completion_events.read))
> +    {
> +        CloseHandle(sock->completion_events.read);
> +    }
> +    if (!is_invalid_handle(sock->completion_events.write))
> +    {
> +        CloseHandle(sock->completion_events.write);
> +    }
> +
> +    free(sock);
> +}
> +
> +static openvpn_transport_socket_t
> +obfs_test_win32_bind(void *plugin_handle, openvpn_transport_args_t args,
> +                     const struct sockaddr *addr,
> openvpn_transport_socklen_t len)
> +{
> +    struct obfs_test_socket_win32 *sock = NULL;
> +    struct sockaddr *addr_rev = NULL;
> +
> +    /* TODO: would be nice to factor out some of these sequences */
> +    addr_rev = calloc(1, len);
> +    if (!addr_rev)
> +    {
> +        goto error;
> +    }
> +    memcpy(addr_rev, addr, len);
> +    obfs_test_munge_addr(addr_rev, len);
> +
> +    sock = calloc(1, sizeof(struct obfs_test_socket_win32));
> +    if (!sock)
> +    {
> +        goto error;
> +    }
> +    sock->handle.vtab = &obfs_test_socket_vtab;
> +    sock->ctx = (struct obfs_test_context *) plugin_handle;
> +    memcpy(&sock->args, args, sizeof(sock->args));
> +
> +    /* Preemptively initialize the members of some Win32 types so error
> exits are okay later on.
> +     * HANDLEs of NULL are considered invalid per above. */
> +    sock->socket = INVALID_SOCKET;
> +
> +    sock->socket = socket(addr_rev->sa_family, SOCK_DGRAM, IPPROTO_UDP);
> +    if (sock->socket == INVALID_SOCKET)
> +    {
> +        goto error;
> +    }
> +
> +    /* See above: write is ready when idle, read is not-ready when idle.
> */
> +    sock->completion_events.read = CreateEvent(NULL, TRUE, FALSE, NULL);
> +    sock->completion_events.write = CreateEvent(NULL, TRUE, TRUE, NULL);
> +    if (is_invalid_handle(sock->completion_events.read) ||
> is_invalid_handle(sock->completion_events.write))
> +    {
> +        goto error;
> +    }
> +    if (!setup_io_slot(&sock->slot_read, sock->ctx,
> +                       sock->socket, sock->completion_events.read))
> +    {
> +        goto error;
> +    }
> +    if (!setup_io_slot(&sock->slot_write, sock->ctx,
> +                       sock->socket, sock->completion_events.write))
> +    {
> +        goto error;
> +    }
> +
> +    if (bind(sock->socket, addr_rev, len))
> +    {
> +        goto error;
> +    }
> +    free(addr_rev);
> +    return &sock->handle;
> +
> +error:
> +    obfs_test_log((struct obfs_test_context *) plugin_handle, PLOG_ERR,
> +                  "bind failure: WSA error = %d", WSAGetLastError());
> +    free_socket(sock);
> +    free(addr_rev);
> +    return NULL;
> +}
> +
> +static void
> +handle_sendrecv_return(struct io_slot *slot, int status)
> +{
> +    if (status == 0)
> +    {
> +        /* Immediately completed. Set the event so it stays consistent. */
> +        slot->status = IO_SLOT_COMPLETE;
> +        slot->succeeded = true;
> +        slot->buf_len = slot->bytes;
> +        SetEvent(slot->overlapped.hEvent);
> +    }
> +    else if (WSAGetLastError() == WSA_IO_PENDING)
> +    {
> +        /* Queued. */
> +        slot->status = IO_SLOT_PENDING;
> +    }
> +    else
> +    {
> +        /* Error. */
> +        slot->status = IO_SLOT_COMPLETE;
> +        slot->succeeded = false;
> +        slot->wsa_error = WSAGetLastError();
> +        slot->buf_len = 0;
> +    }
> +}
> +
> +static void
> +queue_new_read(struct io_slot *slot, size_t cap)
> +{
> +    int status;
> +    WSABUF sbuf;
> +    assert(slot->status == IO_SLOT_DORMANT);
> +
> +    ResetEvent(slot->overlapped.hEvent);
> +    resize_io_buf(slot, cap);
> +    sbuf.buf = slot->buf;
> +    sbuf.len = slot->buf_cap;
> +    slot->addr_len = slot->addr_cap;
> +    slot->flags = 0;
> +    status = WSARecvFrom(slot->socket, &sbuf, 1, &slot->bytes,
> &slot->flags,
> +                         (struct sockaddr *)&slot->addr, &slot->addr_len,
> +                         &slot->overlapped, NULL);
> +    handle_sendrecv_return(slot, status);
> +}
> +
> +/* write slot buffer must already be full. */
> +static void
> +queue_new_write(struct io_slot *slot)
> +{
> +    int status;
> +    WSABUF sbuf;
> +    assert(slot->status == IO_SLOT_COMPLETE || slot->status ==
> IO_SLOT_DORMANT);
> +
> +    ResetEvent(slot->overlapped.hEvent);
> +    sbuf.buf = slot->buf;
> +    sbuf.len = slot->buf_len;
> +    slot->flags = 0;
> +    status = WSASendTo(slot->socket, &sbuf, 1, &slot->bytes, 0 /* flags
> */,
> +                       (struct sockaddr *)&slot->addr, slot->addr_len,
> +                       &slot->overlapped, NULL);
> +    handle_sendrecv_return(slot, status);
> +}
> +
> +static void
> +ensure_pending_read(struct obfs_test_socket_win32 *sock)
> +{
> +    struct io_slot *slot = &sock->slot_read;
> +    switch (slot->status)
> +    {
> +        case IO_SLOT_PENDING:
> +            return;
> +
> +        case IO_SLOT_COMPLETE:
> +            /* Set the event manually here just in case. */
> +            SetEvent(slot->overlapped.hEvent);
> +            return;
> +
> +        case IO_SLOT_DORMANT:
> +            /* TODO: we don't propagate max read size here, so we just
> have to assume the maximum. */
> +            queue_new_read(slot, 65536);
> +            return;
> +
> +        default:
> +            abort();
> +    }
> +}
> +
> +static bool
> +complete_pending_operation(struct io_slot *slot)
> +{
> +    DWORD bytes, flags;
> +    BOOL ok;
> +
> +    switch (slot->status)
> +    {
> +        case IO_SLOT_DORMANT:
> +            /* TODO: shouldn't get here? */
> +            return false;
> +
> +        case IO_SLOT_COMPLETE:
> +            return true;
> +
> +        case IO_SLOT_PENDING:
> +            ok = WSAGetOverlappedResult(slot->socket, &slot->overlapped,
> &bytes,
> +                                        FALSE /* don't wait */, &flags);
> +            if (!ok && WSAGetLastError() == WSA_IO_INCOMPLETE)
> +            {
> +                /* Still waiting. */
> +                return false;
> +            }
> +            else if (ok)
> +            {
> +                /* Completed. slot->addr_len has already been updated. */
> +                slot->buf_len = bytes;
> +                slot->status = IO_SLOT_COMPLETE;
> +                slot->succeeded = true;
> +                return true;
> +            }
> +            else
> +            {
> +                /* Error. */
> +                slot->buf_len = 0;
> +                slot->status = IO_SLOT_COMPLETE;
> +                slot->succeeded = false;
> +                slot->wsa_error = WSAGetLastError();
> +                return true;
> +            }
> +
> +        default:
> +            abort();
> +    }
> +}
> +
> +static bool
> +complete_pending_read(struct obfs_test_socket_win32 *sock)
> +{
> +    bool done = complete_pending_operation(&sock->slot_read);
> +    if (done)
> +    {
> +        ResetEvent(sock->completion_events.read);
> +    }
> +    return done;
> +}
> +
> +static void
> +consumed_pending_read(struct obfs_test_socket_win32 *sock)
> +{
> +    struct io_slot *slot = &sock->slot_read;
> +    assert(slot->status == IO_SLOT_COMPLETE);
> +    slot->status = IO_SLOT_DORMANT;
> +    slot->succeeded = false;
> +    ResetEvent(slot->overlapped.hEvent);
> +}
> +
> +static inline bool
> +complete_pending_write(struct obfs_test_socket_win32 *sock)
> +{
> +    bool done = complete_pending_operation(&sock->slot_write);
> +    if (done)
> +    {
> +        SetEvent(sock->completion_events.write);
> +    }
> +    return done;
> +}
> +
> +static void
> +obfs_test_win32_request_event(openvpn_transport_socket_t handle,
> +                              openvpn_transport_event_set_handle_t
> event_set, unsigned rwflags)
> +{
> +    struct obfs_test_socket_win32 *sock = (struct obfs_test_socket_win32
> *)handle;
> +    obfs_test_log(sock->ctx, PLOG_DEBUG, "request-event: %d", rwflags);
> +    sock->last_rwflags = 0;
> +
> +    if (rwflags & OPENVPN_TRANSPORT_EVENT_READ)
> +    {
> +        ensure_pending_read(sock);
> +    }
> +    if (rwflags)
> +    {
> +        event_set->vtab->set_event(event_set, &sock->completion_events,
> rwflags, handle);
> +    }
> +}
> +
> +static bool
> +obfs_test_win32_update_event(openvpn_transport_socket_t handle, void
> *arg, unsigned rwflags)
> +{
> +    obfs_test_log(((struct obfs_test_socket_win32 *) handle)->ctx,
> PLOG_DEBUG,
> +                  "update-event: %p, %p, %d", handle, arg, rwflags);
> +    if (arg != handle)
> +    {
> +        return false;
> +    }
> +    ((struct obfs_test_socket_win32 *) handle)->last_rwflags |= rwflags;
> +    return true;
> +}
> +
> +static unsigned
> +obfs_test_win32_pump(openvpn_transport_socket_t handle)
> +{
> +    struct obfs_test_socket_win32 *sock = (struct obfs_test_socket_win32
> *)handle;
> +    unsigned result = 0;
> +
> +    if ((sock->last_rwflags & OPENVPN_TRANSPORT_EVENT_READ) &&
> complete_pending_read(sock))
> +    {
> +        result |= OPENVPN_TRANSPORT_EVENT_READ;
> +    }
> +    if ((sock->last_rwflags & OPENVPN_TRANSPORT_EVENT_WRITE)
> +        && (sock->slot_write.status != IO_SLOT_PENDING ||
> complete_pending_write(sock)))
> +    {
> +        result |= OPENVPN_TRANSPORT_EVENT_WRITE;
> +    }
> +
> +    obfs_test_log(sock->ctx, PLOG_DEBUG, "pump -> %d", result);
> +    return result;
> +}
> +
> +static ssize_t
> +obfs_test_win32_recvfrom(openvpn_transport_socket_t handle, void *buf,
> size_t len,
> +                         struct sockaddr *addr,
> openvpn_transport_socklen_t *addrlen)
> +{
> +    struct obfs_test_socket_win32 *sock = (struct obfs_test_socket_win32
> *)handle;
> +    if (!complete_pending_read(sock))
> +    {
> +        WSASetLastError(WSA_IO_INCOMPLETE);
> +        return -1;
> +    }
> +
> +    if (!sock->slot_read.succeeded)
> +    {
> +        int wsa_error = sock->slot_read.wsa_error;
> +        consumed_pending_read(sock);
> +        WSASetLastError(wsa_error);
> +        return -1;
> +    }
> +
> +    /* sock->slot_read now has valid data. */
> +    char *working_buf = sock->slot_read.buf;
> +    ssize_t unmunged_len =
> +        obfs_test_unmunge_buf(&sock->args, working_buf,
> +                              sock->slot_read.buf_len);
> +    if (unmunged_len < 0)
> +    {
> +        /* Act as though this read never happened. Assume one was queued
> before, so it should
> +         * still remain queued. */
> +        consumed_pending_read(sock);
> +        ensure_pending_read(sock);
> +        WSASetLastError(WSA_IO_INCOMPLETE);
> +        return -1;
> +    }
> +
> +    size_t copy_len = unmunged_len;
> +    if (copy_len > len)
> +    {
> +        copy_len = len;
> +    }
> +    memcpy(buf, sock->slot_read.buf, copy_len);
> +
> +    /* TODO: shouldn't truncate, should signal error (but this shouldn't
> happen for any
> +     * supported address families anyway). */
> +    openvpn_transport_socklen_t addr_copy_len = *addrlen;
> +    if (sock->slot_read.addr_len < addr_copy_len)
> +    {
> +        addr_copy_len = sock->slot_read.addr_len;
> +    }
> +    memcpy(addr, &sock->slot_read.addr, addr_copy_len);
> +    *addrlen = addr_copy_len;
> +    if (addr_copy_len > 0)
> +    {
> +        obfs_test_munge_addr(addr, addr_copy_len);
> +    }
> +
> +    /* Reset the I/O slot before returning. */
> +    consumed_pending_read(sock);
> +    return copy_len;
> +}
> +
> +static ssize_t
> +obfs_test_win32_sendto(openvpn_transport_socket_t handle, const void
> *buf, size_t len,
> +                       const struct sockaddr *addr,
> openvpn_transport_socklen_t addrlen)
> +{
> +    struct obfs_test_socket_win32 *sock = (struct obfs_test_socket_win32
> *)handle;
> +    complete_pending_write(sock);
> +
> +    if (sock->slot_write.status == IO_SLOT_PENDING)
> +    {
> +        /* This shouldn't really happen, but. */
> +        WSASetLastError(WSAEWOULDBLOCK);
> +        return -1;
> +    }
> +
> +    if (addrlen > sock->slot_write.addr_cap)
> +    {
> +        /* Shouldn't happen. */
> +        WSASetLastError(WSAEFAULT);
> +        return -1;
> +    }
> +
> +    /* TODO: propagate previous write errors---what does core expect
> here? */
> +    memcpy(&sock->slot_write.addr, addr, addrlen);
> +    sock->slot_write.addr_len = addrlen;
> +    if (addrlen > 0)
> +    {
> +        obfs_test_munge_addr((struct sockaddr *)&sock->slot_write.addr,
> addrlen);
> +    }
> +    resize_io_buf(&sock->slot_write, obfs_test_max_munged_buf_size(len));
> +    sock->slot_write.buf_len =
> +        obfs_test_munge_buf(&sock->args, sock->slot_write.buf, buf, len);
> +    queue_new_write(&sock->slot_write);
> +    switch (sock->slot_write.status)
> +    {
> +        case IO_SLOT_PENDING:
> +            /* The network hasn't given us an error yet, but _we've_
> consumed all the bytes.
> +             * ... sort of. */
> +            return len;
> +
> +        case IO_SLOT_DORMANT:
> +            /* Huh?? But we just queued a write. */
> +            abort();
> +
> +        case IO_SLOT_COMPLETE:
> +            if (sock->slot_write.succeeded)
> +            {
> +                /* TODO: more partial length handling */
> +                return len;
> +            }
> +            else
> +            {
> +                return -1;
> +            }
> +
> +        default:
> +            abort();
> +    }
> +}
> +
> +static void
> +obfs_test_win32_close(openvpn_transport_socket_t handle)
> +{
> +    free_socket((struct obfs_test_socket_win32 *) handle);
> +}
> +
> +void
> +obfs_test_initialize_vtabs_platform(void)
> +{
> +    obfs_test_bind_vtab.bind = obfs_test_win32_bind;
> +    obfs_test_socket_vtab.request_event = obfs_test_win32_request_event;
> +    obfs_test_socket_vtab.update_event = obfs_test_win32_update_event;
> +    obfs_test_socket_vtab.pump = obfs_test_win32_pump;
> +    obfs_test_socket_vtab.recvfrom = obfs_test_win32_recvfrom;
> +    obfs_test_socket_vtab.sendto = obfs_test_win32_sendto;
> +    obfs_test_socket_vtab.close = obfs_test_win32_close;
> +}
> diff --git a/src/plugins/obfs-test/obfs-test.c
> b/src/plugins/obfs-test/obfs-test.c
> new file mode 100644
> index 00000000..27a3d21e
> --- /dev/null
> +++ b/src/plugins/obfs-test/obfs-test.c
> @@ -0,0 +1,94 @@
> +#include <stdlib.h>
> +#include <string.h>
> +#include <stdbool.h>
> +#include "openvpn-plugin.h"
> +#include "openvpn-transport.h"
> +#include "obfs-test.h"
> +
> +struct openvpn_transport_bind_vtab1 obfs_test_bind_vtab = { 0 };
> +struct openvpn_transport_socket_vtab1 obfs_test_socket_vtab = { 0 };
> +
> +struct obfs_test_context
> +{
> +    struct openvpn_plugin_callbacks *global_vtab;
> +};
> +
> +static void
> +free_context(struct obfs_test_context *context)
> +{
> +    if (!context)
> +    {
> +        return;
> +    }
> +    free(context);
> +}
> +
> +OPENVPN_EXPORT int
> +openvpn_plugin_open_v3(int version, struct openvpn_plugin_args_open_in
> const *args,
> +                       struct openvpn_plugin_args_open_return *out)
> +{
> +    struct obfs_test_context *context;
> +
> +    context = (struct obfs_test_context *) calloc(1, sizeof(struct
> obfs_test_context));
> +    if (!context)
> +    {
> +        return OPENVPN_PLUGIN_FUNC_ERROR;
> +    }
> +
> +    context->global_vtab = args->callbacks;
> +    obfs_test_initialize_vtabs_platform();
> +    obfs_test_bind_vtab.parseargs = obfs_test_parseargs;
> +    obfs_test_bind_vtab.argerror = obfs_test_argerror;
> +    obfs_test_bind_vtab.freeargs = obfs_test_freeargs;
> +
> +    out->type_mask = OPENVPN_PLUGIN_MASK(OPENVPN_PLUGIN_TRANSPORT);
> +    out->handle = (openvpn_plugin_handle_t *) context;
> +    return OPENVPN_PLUGIN_FUNC_SUCCESS;
> +
> +err:
> +    free_context(context);
> +    return OPENVPN_PLUGIN_FUNC_ERROR;
> +}
> +
> +OPENVPN_EXPORT void
> +openvpn_plugin_close_v1(openvpn_plugin_handle_t handle)
> +{
> +    free_context((struct obfs_test_context *) handle);
> +}
> +
> +OPENVPN_EXPORT int
> +openvpn_plugin_func_v3(int version,
> +                       struct openvpn_plugin_args_func_in const
> *arguments,
> +                       struct openvpn_plugin_args_func_return *retptr)
> +{
> +    /* We don't ask for any bits that use this interface. */
> +    return OPENVPN_PLUGIN_FUNC_ERROR;
> +}
> +
> +OPENVPN_EXPORT void *
> +openvpn_plugin_get_vtab_v1(int selector, size_t *size_out)
> +{
> +    switch (selector)
> +    {
> +        case OPENVPN_VTAB_TRANSPORT_BIND_V1:
> +            if (obfs_test_bind_vtab.bind == NULL)
> +            {
> +                return NULL;
> +            }
> +            *size_out = sizeof(struct openvpn_transport_bind_vtab1);
> +            return &obfs_test_bind_vtab;
> +
> +        default:
> +            return NULL;
> +    }
> +}
> +
> +void
> +obfs_test_log(struct obfs_test_context *ctx,
> +              openvpn_plugin_log_flags_t flags, const char *fmt, ...)
> +{
> +    va_list va;
> +    va_start(va, fmt);
> +    ctx->global_vtab->plugin_vlog(flags, OBFS_TEST_PLUGIN_NAME, fmt, va);
> +    va_end(va);
> +}
> diff --git a/src/plugins/obfs-test/obfs-test.exports
> b/src/plugins/obfs-test/obfs-test.exports
> new file mode 100644
> index 00000000..e7baada4
> --- /dev/null
> +++ b/src/plugins/obfs-test/obfs-test.exports
> @@ -0,0 +1,4 @@
> +openvpn_plugin_open_v3
> +openvpn_plugin_close_v1
> +openvpn_plugin_get_vtab_v1
> +openvpn_plugin_func_v3
> diff --git a/src/plugins/obfs-test/obfs-test.h
> b/src/plugins/obfs-test/obfs-test.h
> new file mode 100644
> index 00000000..b9a6f8b4
> --- /dev/null
> +++ b/src/plugins/obfs-test/obfs-test.h
> @@ -0,0 +1,42 @@
> +#ifndef OPENVPN_PLUGIN_OBFS_TEST_H
> +#define OPENVPN_PLUGIN_OBFS_TEST_H 1
> +
> +#include "openvpn-plugin.h"
> +#include "openvpn-transport.h"
> +
> +#define OBFS_TEST_PLUGIN_NAME "obfs-test"
> +
> +struct obfs_test_context;
> +
> +struct obfs_test_args
> +{
> +    const char *error;
> +    int offset;
> +};
> +
> +extern struct openvpn_transport_bind_vtab1 obfs_test_bind_vtab;
> +extern struct openvpn_transport_socket_vtab1 obfs_test_socket_vtab;
> +
> +void obfs_test_initialize_vtabs_platform(void);
> +
> +void obfs_test_munge_addr(struct sockaddr *addr,
> openvpn_transport_socklen_t len);
> +
> +size_t obfs_test_max_munged_buf_size(size_t clear_size);
> +
> +size_t obfs_test_munge_buf(struct obfs_test_args *how,
> +                           char *out, const char *in, size_t len);
> +
> +ssize_t obfs_test_unmunge_buf(struct obfs_test_args *how,
> +                              char *buf, size_t len);
> +
> +openvpn_transport_args_t obfs_test_parseargs(void *plugin_handle,
> +                                             const char *const *argv, int
> argc);
> +
> +const char *obfs_test_argerror(openvpn_transport_args_t args);
> +
> +void obfs_test_freeargs(openvpn_transport_args_t args);
> +
> +void obfs_test_log(struct obfs_test_context *ctx,
> +                   openvpn_plugin_log_flags_t flags, const char *fmt,
> ...);
> +
> +#endif /* !OPENVPN_PLUGIN_OBFS_TEST_H */
> --
> 2.19.2
>
>
>
> _______________________________________________
> Openvpn-devel mailing list
> Openvpn-devel@lists.sourceforge.net
> https://lists.sourceforge.net/lists/listinfo/openvpn-devel
>
<div dir="ltr">Bumping this as well given the holiday hiatus - it seems like there was feedback on the patches 2 &amp; 3. Does anyone have any feedback for this one?<div><br></div><div>Thanks!</div><div>Justin<br clear="all"><div><div dir="ltr" class="gmail_signature" data-smartmail="gmail_signature"><div dir="ltr"><div><div dir="ltr"><div><div dir="ltr"><div><div dir="ltr"><div><div dir="ltr"><div><div dir="ltr"><div dir="ltr"><div dir="ltr"><div dir="ltr"><div dir="ltr"><div dir="ltr"><div dir="ltr"><div dir="ltr"><div dir="ltr"><div dir="ltr"><div dir="ltr"><br><table cellspacing="0" cellpadding="0" style="font-family:&quot;open sans&quot;,sans-serif"><tbody><tr style="color:rgb(102,102,102);font-family:sans-serif;font-size:small"><td nowrap valign="top" style="border:none;padding-right:22px"><img src="https://www.gstatic.com/jigsaw/Jigsaw_logo.png" height="45" width="45" style="height:45px;width:45px"></td><td nowrap style="border:none"><span style="font-weight:bold">Justin Henck</span> <br><span>Product Manager</span><span></span> <br><div style="display:inline"><span>212-565-9811</span> <br></div><a href="https://google.com/jigsaw" style="color:rgb(102,102,102)" target="_blank">google.com/jigsaw</a></td></tr></tbody></table></div><div dir="ltr"><div dir="auto"><div><span><font color="#666666"><br></font></span></div><div><span><font color="#666666">PGP: EA8E 8C27 2D75 974D B357 482B 1039 9F2D 869A 117B</font></span></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div></div><br></div></div><br><div class="gmail_quote"><div dir="ltr" class="gmail_attr">On Sun, Dec 30, 2018 at 6:31 AM Antonio Quartulli &lt;a@unstable.cc&gt; wrote:<br></div><blockquote class="gmail_quote" style="margin:0px 0px 0px 0.8ex;border-left:1px solid rgb(204,204,204);padding-left:1ex">From: Robin Tarsiger &lt;<a href="mailto:rtt@dasyatidae.com" target="_blank">rtt@dasyatidae.com</a>&gt;<br>
<br>
Add a sample plugin to explain how the new transport API is expected to<br>
be implemented and work. It can be used for testing.<br>
<br>
Signed-off-by: Robin Tarsiger &lt;<a href="mailto:rtt@dasyatidae.com" target="_blank">rtt@dasyatidae.com</a>&gt;<br>
[<a href="mailto:antonio@openvpn.net" target="_blank">antonio@openvpn.net</a>: refactored commits, restyled code]<br>
---<br>
 <a href="http://configure.ac" rel="noreferrer" target="_blank">configure.ac</a>                              |   9 +<br>
 src/plugins/Makefile.am                   |   2 +-<br>
 src/plugins/obfs-test/Makefile.am         |  29 ++<br>
 src/plugins/obfs-test/README.obfs-test    |  26 +<br>
 src/plugins/obfs-test/obfs-test-args.c    |  60 +++<br>
 src/plugins/obfs-test/obfs-test-munging.c | 129 +++++<br>
 src/plugins/obfs-test/obfs-test-posix.c   | 207 ++++++++<br>
 src/plugins/obfs-test/obfs-test-win32.c   | 579 ++++++++++++++++++++++<br>
 src/plugins/obfs-test/obfs-test.c         |  94 ++++<br>
 src/plugins/obfs-test/obfs-test.exports   |   4 +<br>
 src/plugins/obfs-test/obfs-test.h         |  42 ++<br>
 11 files changed, 1180 insertions(+), 1 deletion(-)<br>
 create mode 100644 src/plugins/obfs-test/Makefile.am<br>
 create mode 100644 src/plugins/obfs-test/README.obfs-test<br>
 create mode 100644 src/plugins/obfs-test/obfs-test-args.c<br>
 create mode 100644 src/plugins/obfs-test/obfs-test-munging.c<br>
 create mode 100644 src/plugins/obfs-test/obfs-test-posix.c<br>
 create mode 100644 src/plugins/obfs-test/obfs-test-win32.c<br>
 create mode 100644 src/plugins/obfs-test/obfs-test.c<br>
 create mode 100644 src/plugins/obfs-test/obfs-test.exports<br>
 create mode 100644 src/plugins/obfs-test/obfs-test.h<br>
<br>
diff --git a/<a href="http://configure.ac" rel="noreferrer" target="_blank">configure.ac</a> b/<a href="http://configure.ac" rel="noreferrer" target="_blank">configure.ac</a><br>
index 1e6891b1..b4196812 100644<br>
--- a/<a href="http://configure.ac" rel="noreferrer" target="_blank">configure.ac</a><br>
+++ b/<a href="http://configure.ac" rel="noreferrer" target="_blank">configure.ac</a><br>
@@ -200,6 +200,13 @@ AC_ARG_ENABLE(<br>
        ]<br>
 )<br>
<br>
+AC_ARG_ENABLE(<br>
+       [plugin-obfs-test],<br>
+       [AS_HELP_STRING([--disable-plugin-obfs-test], [disable obfs-test plugin @&lt;:@default=platform specific@:&gt;@])],<br>
+       ,<br>
+       [enable_plugin_obfs_test=&quot;no&quot;]<br>
+)<br>
+<br>
 AC_ARG_ENABLE(<br>
        [pam-dlopen],<br>
        [AS_HELP_STRING([--enable-pam-dlopen], [dlopen libpam @&lt;:@default=no@:&gt;@])],<br>
@@ -1344,6 +1351,7 @@ AM_CONDITIONAL([WIN32], [test &quot;${WIN32}&quot; = &quot;yes&quot;])<br>
 AM_CONDITIONAL([GIT_CHECKOUT], [test &quot;${GIT_CHECKOUT}&quot; = &quot;yes&quot;])<br>
 AM_CONDITIONAL([ENABLE_PLUGIN_AUTH_PAM], [test &quot;${enable_plugin_auth_pam}&quot; = &quot;yes&quot;])<br>
 AM_CONDITIONAL([ENABLE_PLUGIN_DOWN_ROOT], [test &quot;${enable_plugin_down_root}&quot; = &quot;yes&quot;])<br>
+AM_CONDITIONAL([ENABLE_PLUGIN_OBFS_TEST], [test &quot;${enable_plugin_obfs_test}&quot; = &quot;yes&quot;])<br>
 AM_CONDITIONAL([HAVE_LD_WRAP_SUPPORT], [test &quot;${have_ld_wrap_support}&quot; = &quot;yes&quot;])<br>
<br>
 sampledir=&quot;\$(docdir)/sample&quot;<br>
@@ -1403,6 +1411,7 @@ AC_CONFIG_FILES([<br>
        src/plugins/Makefile<br>
        src/plugins/auth-pam/Makefile<br>
        src/plugins/down-root/Makefile<br>
+       src/plugins/obfs-test/Makefile<br>
        tests/Makefile<br>
         tests/unit_tests/Makefile<br>
         tests/unit_tests/example_test/Makefile<br>
diff --git a/src/plugins/Makefile.am b/src/plugins/Makefile.am<br>
index f3461786..848bac03 100644<br>
--- a/src/plugins/Makefile.am<br>
+++ b/src/plugins/Makefile.am<br>
@@ -12,4 +12,4 @@<br>
 MAINTAINERCLEANFILES = \<br>
        $(srcdir)/Makefile.in<br>
<br>
-SUBDIRS = auth-pam down-root<br>
+SUBDIRS = auth-pam down-root obfs-test<br>
diff --git a/src/plugins/obfs-test/Makefile.am b/src/plugins/obfs-test/Makefile.am<br>
new file mode 100644<br>
index 00000000..4cc8d183<br>
--- /dev/null<br>
+++ b/src/plugins/obfs-test/Makefile.am<br>
@@ -0,0 +1,29 @@<br>
+MAINTAINERCLEANFILES = \<br>
+       $(srcdir)/Makefile.in<br>
+<br>
+AM_CFLAGS = \<br>
+       -I$(top_srcdir)/include \<br>
+       $(OPTIONAL_CRYPTO_CFLAGS)<br>
+<br>
+if ENABLE_PLUGIN_OBFS_TEST<br>
+plugin_LTLIBRARIES = <a href="http://openvpn-plugin-obfs-test.la" rel="noreferrer" target="_blank">openvpn-plugin-obfs-test.la</a><br>
+endif<br>
+<br>
+openvpn_plugin_obfs_test_la_SOURCES = \<br>
+       obfs-test.c \<br>
+       obfs-test-munging.c \<br>
+       obfs-test-args.c \<br>
+       obfs-test.exports<br>
+<br>
+if WIN32<br>
+openvpn_plugin_obfs_test_la_SOURCES += obfs-test-win32.c<br>
+openvpn_plugin_obfs_test_la_LIBADD = -lws2_32 -lwininet<br>
+else !WIN32<br>
+openvpn_plugin_obfs_test_la_SOURCES += obfs-test-posix.c<br>
+# No LIBADD necessary; we assume we can access the global symbol space,<br>
+# and core OpenVPN will already link with everything needed for sockets.<br>
+endif<br>
+<br>
+openvpn_plugin_obfs_test_la_LDFLAGS = $(AM_LDFLAGS) \<br>
+       -export-symbols &quot;$(srcdir)/obfs-test.exports&quot; \<br>
+       -module -shared -avoid-version -no-undefined<br>
diff --git a/src/plugins/obfs-test/README.obfs-test b/src/plugins/obfs-test/README.obfs-test<br>
new file mode 100644<br>
index 00000000..5492ee02<br>
--- /dev/null<br>
+++ b/src/plugins/obfs-test/README.obfs-test<br>
@@ -0,0 +1,26 @@<br>
+obfs-test<br>
+<br>
+SYNOPSIS<br>
+<br>
+The obfs-test plugin is a proof of concept for supporting protocol<br>
+obfuscation for OpenVPN via a socket intercept plugin.<br>
+<br>
+BUILD<br>
+<br>
+You must specify --enable-plugin-obfs-test at configure time to<br>
+trigger building this plugin. It should function on POSIX-y platforms<br>
+and Windows.<br>
+<br>
+USAGE<br>
+<br>
+To invoke this plugin, load it via an appropriate plugin line in the<br>
+configuration file, and then specify &#39;proto indirect&#39; rather than any<br>
+other protocol. Packets will then be passed via UDP, but they will<br>
+also undergo a very basic content transformation, and the bind port<br>
+will be altered (see obfs-test-munging.c for details).<br>
+<br>
+CAVEATS<br>
+<br>
+This has undergone basic functionality testing, but not any kind of<br>
+full-on stress test. Extended socket or I/O handling options are not<br>
+supported at all.<br>
diff --git a/src/plugins/obfs-test/obfs-test-args.c b/src/plugins/obfs-test/obfs-test-args.c<br>
new file mode 100644<br>
index 00000000..e6756f8f<br>
--- /dev/null<br>
+++ b/src/plugins/obfs-test/obfs-test-args.c<br>
@@ -0,0 +1,60 @@<br>
+#include &quot;obfs-test.h&quot;<br>
+<br>
+openvpn_transport_args_t<br>
+obfs_test_parseargs(void *plugin_handle,<br>
+                    const char *const *argv, int argc)<br>
+{<br>
+    struct obfs_test_args *args = calloc(1, sizeof(struct obfs_test_args));<br>
+    if (!args)<br>
+    {<br>
+        return NULL;<br>
+    }<br>
+<br>
+    if (argc &lt; 2)<br>
+    {<br>
+        args-&gt;offset = 0;<br>
+    }<br>
+    else if (argc == 2)<br>
+    {<br>
+        char *end;<br>
+        long offset = strtol(argv[1], &amp;end, 10);<br>
+        if (*end != &#39;\0&#39;)<br>
+        {<br>
+            args-&gt;error = &quot;offset must be a decimal number&quot;;<br>
+        }<br>
+        else if (!(0 &lt;= offset &amp;&amp; offset &lt;= 42))<br>
+        {<br>
+            args-&gt;error = &quot;offset must be between 0 and 42&quot;;<br>
+        }<br>
+        else<br>
+        {<br>
+            args-&gt;offset = (int) offset;<br>
+        }<br>
+    }<br>
+    else<br>
+    {<br>
+        args-&gt;error = &quot;too many arguments&quot;;<br>
+    }<br>
+<br>
+    return args;<br>
+}<br>
+<br>
+const char *<br>
+obfs_test_argerror(openvpn_transport_args_t args_)<br>
+{<br>
+    if (!args_)<br>
+    {<br>
+        return &quot;cannot allocate&quot;;<br>
+    }<br>
+    else<br>
+    {<br>
+        return ((struct obfs_test_args *) args_)-&gt;error;<br>
+    }<br>
+}<br>
+<br>
+void<br>
+obfs_test_freeargs(openvpn_transport_args_t args_)<br>
+{<br>
+    free(args_);<br>
+    struct obfs_test_args *args = (struct obfs_test_args *) args_;<br>
+}<br>
diff --git a/src/plugins/obfs-test/obfs-test-munging.c b/src/plugins/obfs-test/obfs-test-munging.c<br>
new file mode 100644<br>
index 00000000..37d27039<br>
--- /dev/null<br>
+++ b/src/plugins/obfs-test/obfs-test-munging.c<br>
@@ -0,0 +1,129 @@<br>
+#include &lt;string.h&gt;<br>
+#include &lt;errno.h&gt;<br>
+#include &lt;stdbool.h&gt;<br>
+#include &quot;obfs-test.h&quot;<br>
+#ifdef OPENVPN_TRANSPORT_PLATFORM_POSIX<br>
+#include &lt;sys/socket.h&gt;<br>
+#include &lt;netinet/in.h&gt;<br>
+typedef in_port_t obfs_test_in_port_t;<br>
+#else<br>
+#include &lt;winsock2.h&gt;<br>
+#include &lt;ws2tcpip.h&gt;<br>
+typedef u_short obfs_test_in_port_t;<br>
+#endif<br>
+<br>
+static obfs_test_in_port_t<br>
+munge_port(obfs_test_in_port_t port)<br>
+{<br>
+    return port ^ 15;<br>
+}<br>
+<br>
+/* Reversible. */<br>
+void<br>
+obfs_test_munge_addr(struct sockaddr *addr, openvpn_transport_socklen_t len)<br>
+{<br>
+    struct sockaddr_in *inet;<br>
+    struct sockaddr_in6 *inet6;<br>
+<br>
+    switch (addr-&gt;sa_family)<br>
+    {<br>
+        case AF_INET:<br>
+            inet = (struct sockaddr_in *) addr;<br>
+            inet-&gt;sin_port = munge_port(inet-&gt;sin_port);<br>
+            break;<br>
+<br>
+        case AF_INET6:<br>
+            inet6 = (struct sockaddr_in6 *) addr;<br>
+            inet6-&gt;sin6_port = munge_port(inet6-&gt;sin6_port);<br>
+            break;<br>
+<br>
+        default:<br>
+            break;<br>
+    }<br>
+}<br>
+<br>
+/* Six fixed bytes, six repeated bytes. It&#39;s only a silly transformation. */<br>
+#define MUNGE_OVERHEAD 12<br>
+<br>
+size_t<br>
+obfs_test_max_munged_buf_size(size_t clear_size)<br>
+{<br>
+    return clear_size + MUNGE_OVERHEAD;<br>
+}<br>
+<br>
+ssize_t<br>
+obfs_test_unmunge_buf(struct obfs_test_args *how,<br>
+                      char *buf, size_t len)<br>
+{<br>
+    int i;<br>
+<br>
+    if (len &lt; 6)<br>
+    {<br>
+        goto bad;<br>
+    }<br>
+    for (i = 0; i &lt; 6; i++)<br>
+    {<br>
+        if (buf[i] != i + how-&gt;offset)<br>
+        {<br>
+            goto bad;<br>
+        }<br>
+    }<br>
+<br>
+    for (i = 0; i &lt; 6 &amp;&amp; (6 + 2*i) &lt; len; i++)<br>
+    {<br>
+        if (len &lt; (6 + 2*i + 1) || buf[6 + 2*i] != buf[6 + 2*i + 1])<br>
+        {<br>
+            goto bad;<br>
+        }<br>
+        buf[i] = buf[6 + 2*i];<br>
+    }<br>
+<br>
+    if (len &gt; 18)<br>
+    {<br>
+        memmove(buf + 6, buf + 18, len - 18);<br>
+        len -= 12;<br>
+    }<br>
+    else<br>
+    {<br>
+        len -= 6;<br>
+        len /= 2;<br>
+    }<br>
+<br>
+    return len;<br>
+<br>
+bad:<br>
+    /* TODO: this really isn&#39;t the best way to report this error */<br>
+    errno = EIO;<br>
+    return -1;<br>
+}<br>
+<br>
+/* out must have space for len+MUNGE_OVERHEAD bytes. out and in must<br>
+ * not overlap. */<br>
+size_t<br>
+obfs_test_munge_buf(struct obfs_test_args *how,<br>
+                    char *out, const char *in, size_t len)<br>
+{<br>
+    int i, n;<br>
+    size_t out_len = 6;<br>
+<br>
+    for (i = 0; i &lt; 6; i++)<br>
+    {<br>
+        out[i] = i + how-&gt;offset;<br>
+    }<br>
+    n = len &lt; 6 ? len : 6;<br>
+    for (i = 0; i &lt; n; i++)<br>
+    {<br>
+        out[6 + 2*i] = out[6 + 2*i + 1] = in[i];<br>
+    }<br>
+    if (len &gt; 6)<br>
+    {<br>
+        memmove(out + 18, in + 6, len - 6);<br>
+        out_len = len + 12;<br>
+    }<br>
+    else<br>
+    {<br>
+        out_len = 6 + 2*len;<br>
+    }<br>
+<br>
+    return out_len;<br>
+}<br>
diff --git a/src/plugins/obfs-test/obfs-test-posix.c b/src/plugins/obfs-test/obfs-test-posix.c<br>
new file mode 100644<br>
index 00000000..826381c5<br>
--- /dev/null<br>
+++ b/src/plugins/obfs-test/obfs-test-posix.c<br>
@@ -0,0 +1,207 @@<br>
+#include &quot;obfs-test.h&quot;<br>
+#include &lt;stdbool.h&gt;<br>
+#include &lt;string.h&gt;<br>
+#include &lt;err.h&gt;<br>
+#include &lt;errno.h&gt;<br>
+#include &lt;unistd.h&gt;<br>
+#include &lt;fcntl.h&gt;<br>
+#include &lt;sys/socket.h&gt;<br>
+#include &lt;netinet/in.h&gt;<br>
+<br>
+struct obfs_test_socket_posix<br>
+{<br>
+    struct openvpn_transport_socket handle;<br>
+    struct obfs_test_args args;<br>
+    struct obfs_test_context *ctx;<br>
+    int fd;<br>
+    unsigned last_rwflags;<br>
+};<br>
+<br>
+static void<br>
+free_socket(struct obfs_test_socket_posix *sock)<br>
+{<br>
+    if (!sock)<br>
+    {<br>
+        return;<br>
+    }<br>
+    if (sock-&gt;fd != -1)<br>
+    {<br>
+        close(sock-&gt;fd);<br>
+    }<br>
+    free(sock);<br>
+}<br>
+<br>
+static openvpn_transport_socket_t<br>
+obfs_test_posix_bind(void *plugin_handle, openvpn_transport_args_t args,<br>
+                     const struct sockaddr *addr, socklen_t len)<br>
+{<br>
+    struct obfs_test_socket_posix *sock = NULL;<br>
+    struct sockaddr *addr_rev = NULL;<br>
+<br>
+    addr_rev = calloc(1, len);<br>
+    if (!addr_rev)<br>
+    {<br>
+        goto error;<br>
+    }<br>
+    memcpy(addr_rev, addr, len);<br>
+    obfs_test_munge_addr(addr_rev, len);<br>
+<br>
+    sock = calloc(1, sizeof(struct obfs_test_socket_posix));<br>
+    if (!sock)<br>
+    {<br>
+        goto error;<br>
+    }<br>
+    sock-&gt;handle.vtab = &amp;obfs_test_socket_vtab;<br>
+    sock-&gt;ctx = (struct obfs_test_context *) plugin_handle;<br>
+    memcpy(&amp;sock-&gt;args, args, sizeof(sock-&gt;args));<br>
+    /* Note that sock-&gt;fd isn&#39;t -1 yet. Set it explicitly if there are ever any<br>
+     * error exits before the socket() call. */<br>
+<br>
+    sock-&gt;fd = socket(addr-&gt;sa_family, SOCK_DGRAM, IPPROTO_UDP);<br>
+    if (sock-&gt;fd == -1)<br>
+    {<br>
+        goto error;<br>
+    }<br>
+    if (fcntl(sock-&gt;fd, F_SETFL, fcntl(sock-&gt;fd, F_GETFL) | O_NONBLOCK))<br>
+    {<br>
+        goto error;<br>
+    }<br>
+<br>
+    if (bind(sock-&gt;fd, addr_rev, len))<br>
+    {<br>
+        goto error;<br>
+    }<br>
+    free(addr_rev);<br>
+    return &amp;sock-&gt;handle;<br>
+<br>
+error:<br>
+    free_socket(sock);<br>
+    free(addr_rev);<br>
+    return NULL;<br>
+}<br>
+<br>
+static void<br>
+obfs_test_posix_request_event(openvpn_transport_socket_t handle,<br>
+                              openvpn_transport_event_set_handle_t event_set, unsigned rwflags)<br>
+{<br>
+    obfs_test_log(((struct obfs_test_socket_posix *) handle)-&gt;ctx,<br>
+                  PLOG_DEBUG, &quot;request-event: %d&quot;, rwflags);<br>
+    ((struct obfs_test_socket_posix *) handle)-&gt;last_rwflags = 0;<br>
+    if (rwflags)<br>
+    {<br>
+        event_set-&gt;vtab-&gt;set_event(event_set, ((struct obfs_test_socket_posix *) handle)-&gt;fd,<br>
+                                   rwflags, handle);<br>
+    }<br>
+}<br>
+<br>
+static bool<br>
+obfs_test_posix_update_event(openvpn_transport_socket_t handle, void *arg, unsigned rwflags)<br>
+{<br>
+    obfs_test_log(((struct obfs_test_socket_posix *) handle)-&gt;ctx,<br>
+                  PLOG_DEBUG, &quot;update-event: %p, %p, %d&quot;, handle, arg, rwflags);<br>
+    if (arg != handle)<br>
+    {<br>
+        return false;<br>
+    }<br>
+    ((struct obfs_test_socket_posix *) handle)-&gt;last_rwflags |= rwflags;<br>
+    return true;<br>
+}<br>
+<br>
+static unsigned<br>
+obfs_test_posix_pump(openvpn_transport_socket_t handle)<br>
+{<br>
+    obfs_test_log(((struct obfs_test_socket_posix *) handle)-&gt;ctx,<br>
+                  PLOG_DEBUG, &quot;pump -&gt; %d&quot;, ((struct obfs_test_socket_posix *) handle)-&gt;last_rwflags);<br>
+    return ((struct obfs_test_socket_posix *) handle)-&gt;last_rwflags;<br>
+}<br>
+<br>
+static ssize_t<br>
+obfs_test_posix_recvfrom(openvpn_transport_socket_t handle, void *buf, size_t len,<br>
+                         struct sockaddr *addr, socklen_t *addrlen)<br>
+{<br>
+    int fd = ((struct obfs_test_socket_posix *) handle)-&gt;fd;<br>
+    ssize_t result;<br>
+<br>
+again:<br>
+    result = recvfrom(fd, buf, len, 0, addr, addrlen);<br>
+    if (result &lt; 0 &amp;&amp; errno == EAGAIN)<br>
+    {<br>
+        ((struct obfs_test_socket_posix *) handle)-&gt;last_rwflags &amp;= ~OPENVPN_TRANSPORT_EVENT_READ;<br>
+    }<br>
+    if (*addrlen &gt; 0)<br>
+    {<br>
+        obfs_test_munge_addr(addr, *addrlen);<br>
+    }<br>
+    if (result &gt; 0)<br>
+    {<br>
+        struct obfs_test_args *how = &amp;((struct obfs_test_socket_posix *) handle)-&gt;args;<br>
+        result = obfs_test_unmunge_buf(how, buf, result);<br>
+        if (result &lt; 0)<br>
+        {<br>
+            /* Pretend that read never happened. */<br>
+            goto again;<br>
+        }<br>
+    }<br>
+<br>
+    obfs_test_log(((struct obfs_test_socket_posix *) handle)-&gt;ctx,<br>
+                  PLOG_DEBUG, &quot;recvfrom(%d) -&gt; %d&quot;, (int)len, (int)result);<br>
+    return result;<br>
+}<br>
+<br>
+static ssize_t<br>
+obfs_test_posix_sendto(openvpn_transport_socket_t handle, const void *buf, size_t len,<br>
+                       const struct sockaddr *addr, socklen_t addrlen)<br>
+{<br>
+    int fd = ((struct obfs_test_socket_posix *) handle)-&gt;fd;<br>
+    struct sockaddr *addr_rev = calloc(1, addrlen);<br>
+    void *buf_munged = malloc(obfs_test_max_munged_buf_size(len));<br>
+    size_t len_munged;<br>
+    ssize_t result;<br>
+    if (!addr_rev || !buf_munged)<br>
+    {<br>
+        goto error;<br>
+    }<br>
+<br>
+    memcpy(addr_rev, addr, addrlen);<br>
+    obfs_test_munge_addr(addr_rev, addrlen);<br>
+    struct obfs_test_args *how = &amp;((struct obfs_test_socket_posix *) handle)-&gt;args;<br>
+    len_munged = obfs_test_munge_buf(how, buf_munged, buf, len);<br>
+    result = sendto(fd, buf_munged, len_munged, 0, addr_rev, addrlen);<br>
+    if (result &lt; 0 &amp;&amp; errno == EAGAIN)<br>
+    {<br>
+        ((struct obfs_test_socket_posix *) handle)-&gt;last_rwflags &amp;= ~OPENVPN_TRANSPORT_EVENT_WRITE;<br>
+    }<br>
+    /* TODO: not clear what to do here for partial transfers. */<br>
+    if (result &gt; len)<br>
+    {<br>
+        result = len;<br>
+    }<br>
+    obfs_test_log(((struct obfs_test_socket_posix *) handle)-&gt;ctx,<br>
+                  PLOG_DEBUG, &quot;sendto(%d) -&gt; %d&quot;, (int)len, (int)result);<br>
+    free(addr_rev);<br>
+    free(buf_munged);<br>
+    return result;<br>
+<br>
+error:<br>
+    free(addr_rev);<br>
+    free(buf_munged);<br>
+    return -1;<br>
+}<br>
+<br>
+static void<br>
+obfs_test_posix_close(openvpn_transport_socket_t handle)<br>
+{<br>
+    free_socket((struct obfs_test_socket_posix *) handle);<br>
+}<br>
+<br>
+void<br>
+obfs_test_initialize_vtabs_platform(void)<br>
+{<br>
+    obfs_test_bind_vtab.bind = obfs_test_posix_bind;<br>
+    obfs_test_socket_vtab.request_event = obfs_test_posix_request_event;<br>
+    obfs_test_socket_vtab.update_event = obfs_test_posix_update_event;<br>
+    obfs_test_socket_vtab.pump = obfs_test_posix_pump;<br>
+    obfs_test_socket_vtab.recvfrom = obfs_test_posix_recvfrom;<br>
+    obfs_test_socket_vtab.sendto = obfs_test_posix_sendto;<br>
+    obfs_test_socket_vtab.close = obfs_test_posix_close;<br>
+}<br>
diff --git a/src/plugins/obfs-test/obfs-test-win32.c b/src/plugins/obfs-test/obfs-test-win32.c<br>
new file mode 100644<br>
index 00000000..46c95f55<br>
--- /dev/null<br>
+++ b/src/plugins/obfs-test/obfs-test-win32.c<br>
@@ -0,0 +1,579 @@<br>
+#include &quot;obfs-test.h&quot;<br>
+#include &lt;stdbool.h&gt;<br>
+#include &lt;string.h&gt;<br>
+#include &lt;stdio.h&gt;<br>
+#include &lt;stdarg.h&gt;<br>
+#include &lt;windows.h&gt;<br>
+#include &lt;winsock2.h&gt;<br>
+#include &lt;assert.h&gt;<br>
+<br>
+static inline bool<br>
+is_invalid_handle(HANDLE h)<br>
+{<br>
+    return h == NULL || h == INVALID_HANDLE_VALUE;<br>
+}<br>
+<br>
+typedef enum {<br>
+    IO_SLOT_DORMANT,            /* must be 0 for calloc purposes */<br>
+    IO_SLOT_PENDING,<br>
+    /* success/failure is determined by succeeded flag in COMPLETE state */<br>
+    IO_SLOT_COMPLETE<br>
+} io_slot_status_t;<br>
+<br>
+/* must be calloc&#39;able */<br>
+struct io_slot<br>
+{<br>
+    struct obfs_test_context *ctx;<br>
+    io_slot_status_t status;<br>
+    OVERLAPPED overlapped;<br>
+    SOCKET socket;<br>
+    SOCKADDR_STORAGE addr;<br>
+    int addr_len, addr_cap;<br>
+    DWORD bytes, flags;<br>
+    bool succeeded;<br>
+    int wsa_error;<br>
+<br>
+    /* realloc&#39;d as needed; always private copy, never aliased */<br>
+    char *buf;<br>
+    size_t buf_len, buf_cap;<br>
+};<br>
+<br>
+static bool<br>
+setup_io_slot(struct io_slot *slot, struct obfs_test_context *ctx,<br>
+              SOCKET socket, HANDLE event)<br>
+{<br>
+    slot-&gt;ctx = ctx;<br>
+    slot-&gt;status = IO_SLOT_DORMANT;<br>
+    slot-&gt;addr_cap = sizeof(SOCKADDR_STORAGE);<br>
+    slot-&gt;socket = socket;<br>
+    slot-&gt;overlapped.hEvent = event;<br>
+    return true;<br>
+}<br>
+<br>
+/* Note that this assumes any I/O has already been implicitly canceled (via closesocket),<br>
+ * but not waited for yet. */<br>
+static bool<br>
+destroy_io_slot(struct io_slot *slot)<br>
+{<br>
+    if (slot-&gt;status == IO_SLOT_PENDING)<br>
+    {<br>
+        DWORD bytes, flags;<br>
+        BOOL ok = WSAGetOverlappedResult(slot-&gt;socket, &amp;slot-&gt;overlapped, &amp;bytes,<br>
+                                         TRUE /* wait */, &amp;flags);<br>
+        if (!ok &amp;&amp; WSAGetLastError() == WSA_IO_INCOMPLETE)<br>
+        {<br>
+            obfs_test_log(slot-&gt;ctx, PLOG_ERR,<br>
+                          &quot;destroying I/O slot: canceled operation is still incomplete after wait?!&quot;);<br>
+            return false;<br>
+        }<br>
+    }<br>
+<br>
+    slot-&gt;status = IO_SLOT_DORMANT;<br>
+    return true;<br>
+}<br>
+<br>
+/* FIXME: aborts on error. */<br>
+static void<br>
+resize_io_buf(struct io_slot *slot, size_t cap)<br>
+{<br>
+    if (slot-&gt;buf)<br>
+    {<br>
+        free(slot-&gt;buf);<br>
+        slot-&gt;buf = NULL;<br>
+    }<br>
+<br>
+    char *new_buf = malloc(cap);<br>
+    if (!new_buf)<br>
+    {<br>
+        abort();<br>
+    }<br>
+    slot-&gt;buf = new_buf;<br>
+    slot-&gt;buf_cap = cap;<br>
+}<br>
+<br>
+struct obfs_test_socket_win32<br>
+{<br>
+    struct openvpn_transport_socket handle;<br>
+    struct obfs_test_args args;<br>
+    struct obfs_test_context *ctx;<br>
+    SOCKET socket;<br>
+<br>
+    /* Write is ready when idle; read is not-ready when idle. Both level-triggered. */<br>
+    struct openvpn_transport_win32_event_pair completion_events;<br>
+    struct io_slot slot_read, slot_write;<br>
+<br>
+    int last_rwflags;<br>
+};<br>
+<br>
+static void<br>
+free_socket(struct obfs_test_socket_win32 *sock)<br>
+{<br>
+    /* This only ever becomes false in strange situations where we leak the entire structure for<br>
+     * lack of anything else to do. */<br>
+    bool can_free = true;<br>
+<br>
+    if (!sock)<br>
+    {<br>
+        return;<br>
+    }<br>
+    if (sock-&gt;socket != INVALID_SOCKET)<br>
+    {<br>
+        closesocket(sock-&gt;socket);<br>
+    }<br>
+<br>
+    /* closesocket cancels any pending overlapped I/O, but we still have to potentially<br>
+     * wait for it here before we can free the buffers. This has to happen before closing<br>
+     * the event handles.<br>
+     *<br>
+     * If we can&#39;t figure out when the canceled overlapped I/O is done, for any reason, we defensively<br>
+     * leak the entire structure; freeing it would be permitting the system to corrupt memory later.<br>
+     * TODO: possibly abort() instead, but make sure we&#39;ve handled all the possible &quot;have to try again&quot;<br>
+     * cases above first<br>
+     */<br>
+    if (!destroy_io_slot(&amp;sock-&gt;slot_read))<br>
+    {<br>
+        can_free = false;<br>
+    }<br>
+    if (!destroy_io_slot(&amp;sock-&gt;slot_write))<br>
+    {<br>
+        can_free = false;<br>
+    }<br>
+    if (!can_free)<br>
+    {<br>
+        /* Skip deinitialization of everything else. Doomed. */<br>
+        obfs_test_log(sock-&gt;ctx, PLOG_ERR, &quot;doomed, leaking the entire socket structure&quot;);<br>
+        return;<br>
+    }<br>
+<br>
+    if (!is_invalid_handle(sock-&gt;completion_events.read))<br>
+    {<br>
+        CloseHandle(sock-&gt;completion_events.read);<br>
+    }<br>
+    if (!is_invalid_handle(sock-&gt;completion_events.write))<br>
+    {<br>
+        CloseHandle(sock-&gt;completion_events.write);<br>
+    }<br>
+<br>
+    free(sock);<br>
+}<br>
+<br>
+static openvpn_transport_socket_t<br>
+obfs_test_win32_bind(void *plugin_handle, openvpn_transport_args_t args,<br>
+                     const struct sockaddr *addr, openvpn_transport_socklen_t len)<br>
+{<br>
+    struct obfs_test_socket_win32 *sock = NULL;<br>
+    struct sockaddr *addr_rev = NULL;<br>
+<br>
+    /* TODO: would be nice to factor out some of these sequences */<br>
+    addr_rev = calloc(1, len);<br>
+    if (!addr_rev)<br>
+    {<br>
+        goto error;<br>
+    }<br>
+    memcpy(addr_rev, addr, len);<br>
+    obfs_test_munge_addr(addr_rev, len);<br>
+<br>
+    sock = calloc(1, sizeof(struct obfs_test_socket_win32));<br>
+    if (!sock)<br>
+    {<br>
+        goto error;<br>
+    }<br>
+    sock-&gt;handle.vtab = &amp;obfs_test_socket_vtab;<br>
+    sock-&gt;ctx = (struct obfs_test_context *) plugin_handle;<br>
+    memcpy(&amp;sock-&gt;args, args, sizeof(sock-&gt;args));<br>
+<br>
+    /* Preemptively initialize the members of some Win32 types so error exits are okay later on.<br>
+     * HANDLEs of NULL are considered invalid per above. */<br>
+    sock-&gt;socket = INVALID_SOCKET;<br>
+<br>
+    sock-&gt;socket = socket(addr_rev-&gt;sa_family, SOCK_DGRAM, IPPROTO_UDP);<br>
+    if (sock-&gt;socket == INVALID_SOCKET)<br>
+    {<br>
+        goto error;<br>
+    }<br>
+<br>
+    /* See above: write is ready when idle, read is not-ready when idle. */<br>
+    sock-&gt;completion_events.read = CreateEvent(NULL, TRUE, FALSE, NULL);<br>
+    sock-&gt;completion_events.write = CreateEvent(NULL, TRUE, TRUE, NULL);<br>
+    if (is_invalid_handle(sock-&gt;completion_events.read) || is_invalid_handle(sock-&gt;completion_events.write))<br>
+    {<br>
+        goto error;<br>
+    }<br>
+    if (!setup_io_slot(&amp;sock-&gt;slot_read, sock-&gt;ctx,<br>
+                       sock-&gt;socket, sock-&gt;completion_events.read))<br>
+    {<br>
+        goto error;<br>
+    }<br>
+    if (!setup_io_slot(&amp;sock-&gt;slot_write, sock-&gt;ctx,<br>
+                       sock-&gt;socket, sock-&gt;completion_events.write))<br>
+    {<br>
+        goto error;<br>
+    }<br>
+<br>
+    if (bind(sock-&gt;socket, addr_rev, len))<br>
+    {<br>
+        goto error;<br>
+    }<br>
+    free(addr_rev);<br>
+    return &amp;sock-&gt;handle;<br>
+<br>
+error:<br>
+    obfs_test_log((struct obfs_test_context *) plugin_handle, PLOG_ERR,<br>
+                  &quot;bind failure: WSA error = %d&quot;, WSAGetLastError());<br>
+    free_socket(sock);<br>
+    free(addr_rev);<br>
+    return NULL;<br>
+}<br>
+<br>
+static void<br>
+handle_sendrecv_return(struct io_slot *slot, int status)<br>
+{<br>
+    if (status == 0)<br>
+    {<br>
+        /* Immediately completed. Set the event so it stays consistent. */<br>
+        slot-&gt;status = IO_SLOT_COMPLETE;<br>
+        slot-&gt;succeeded = true;<br>
+        slot-&gt;buf_len = slot-&gt;bytes;<br>
+        SetEvent(slot-&gt;overlapped.hEvent);<br>
+    }<br>
+    else if (WSAGetLastError() == WSA_IO_PENDING)<br>
+    {<br>
+        /* Queued. */<br>
+        slot-&gt;status = IO_SLOT_PENDING;<br>
+    }<br>
+    else<br>
+    {<br>
+        /* Error. */<br>
+        slot-&gt;status = IO_SLOT_COMPLETE;<br>
+        slot-&gt;succeeded = false;<br>
+        slot-&gt;wsa_error = WSAGetLastError();<br>
+        slot-&gt;buf_len = 0;<br>
+    }<br>
+}<br>
+<br>
+static void<br>
+queue_new_read(struct io_slot *slot, size_t cap)<br>
+{<br>
+    int status;<br>
+    WSABUF sbuf;<br>
+    assert(slot-&gt;status == IO_SLOT_DORMANT);<br>
+<br>
+    ResetEvent(slot-&gt;overlapped.hEvent);<br>
+    resize_io_buf(slot, cap);<br>
+    sbuf.buf = slot-&gt;buf;<br>
+    sbuf.len = slot-&gt;buf_cap;<br>
+    slot-&gt;addr_len = slot-&gt;addr_cap;<br>
+    slot-&gt;flags = 0;<br>
+    status = WSARecvFrom(slot-&gt;socket, &amp;sbuf, 1, &amp;slot-&gt;bytes, &amp;slot-&gt;flags,<br>
+                         (struct sockaddr *)&amp;slot-&gt;addr, &amp;slot-&gt;addr_len,<br>
+                         &amp;slot-&gt;overlapped, NULL);<br>
+    handle_sendrecv_return(slot, status);<br>
+}<br>
+<br>
+/* write slot buffer must already be full. */<br>
+static void<br>
+queue_new_write(struct io_slot *slot)<br>
+{<br>
+    int status;<br>
+    WSABUF sbuf;<br>
+    assert(slot-&gt;status == IO_SLOT_COMPLETE || slot-&gt;status == IO_SLOT_DORMANT);<br>
+<br>
+    ResetEvent(slot-&gt;overlapped.hEvent);<br>
+    sbuf.buf = slot-&gt;buf;<br>
+    sbuf.len = slot-&gt;buf_len;<br>
+    slot-&gt;flags = 0;<br>
+    status = WSASendTo(slot-&gt;socket, &amp;sbuf, 1, &amp;slot-&gt;bytes, 0 /* flags */,<br>
+                       (struct sockaddr *)&amp;slot-&gt;addr, slot-&gt;addr_len,<br>
+                       &amp;slot-&gt;overlapped, NULL);<br>
+    handle_sendrecv_return(slot, status);<br>
+}<br>
+<br>
+static void<br>
+ensure_pending_read(struct obfs_test_socket_win32 *sock)<br>
+{<br>
+    struct io_slot *slot = &amp;sock-&gt;slot_read;<br>
+    switch (slot-&gt;status)<br>
+    {<br>
+        case IO_SLOT_PENDING:<br>
+            return;<br>
+<br>
+        case IO_SLOT_COMPLETE:<br>
+            /* Set the event manually here just in case. */<br>
+            SetEvent(slot-&gt;overlapped.hEvent);<br>
+            return;<br>
+<br>
+        case IO_SLOT_DORMANT:<br>
+            /* TODO: we don&#39;t propagate max read size here, so we just have to assume the maximum. */<br>
+            queue_new_read(slot, 65536);<br>
+            return;<br>
+<br>
+        default:<br>
+            abort();<br>
+    }<br>
+}<br>
+<br>
+static bool<br>
+complete_pending_operation(struct io_slot *slot)<br>
+{<br>
+    DWORD bytes, flags;<br>
+    BOOL ok;<br>
+<br>
+    switch (slot-&gt;status)<br>
+    {<br>
+        case IO_SLOT_DORMANT:<br>
+            /* TODO: shouldn&#39;t get here? */<br>
+            return false;<br>
+<br>
+        case IO_SLOT_COMPLETE:<br>
+            return true;<br>
+<br>
+        case IO_SLOT_PENDING:<br>
+            ok = WSAGetOverlappedResult(slot-&gt;socket, &amp;slot-&gt;overlapped, &amp;bytes,<br>
+                                        FALSE /* don&#39;t wait */, &amp;flags);<br>
+            if (!ok &amp;&amp; WSAGetLastError() == WSA_IO_INCOMPLETE)<br>
+            {<br>
+                /* Still waiting. */<br>
+                return false;<br>
+            }<br>
+            else if (ok)<br>
+            {<br>
+                /* Completed. slot-&gt;addr_len has already been updated. */<br>
+                slot-&gt;buf_len = bytes;<br>
+                slot-&gt;status = IO_SLOT_COMPLETE;<br>
+                slot-&gt;succeeded = true;<br>
+                return true;<br>
+            }<br>
+            else<br>
+            {<br>
+                /* Error. */<br>
+                slot-&gt;buf_len = 0;<br>
+                slot-&gt;status = IO_SLOT_COMPLETE;<br>
+                slot-&gt;succeeded = false;<br>
+                slot-&gt;wsa_error = WSAGetLastError();<br>
+                return true;<br>
+            }<br>
+<br>
+        default:<br>
+            abort();<br>
+    }<br>
+}<br>
+<br>
+static bool<br>
+complete_pending_read(struct obfs_test_socket_win32 *sock)<br>
+{<br>
+    bool done = complete_pending_operation(&amp;sock-&gt;slot_read);<br>
+    if (done)<br>
+    {<br>
+        ResetEvent(sock-&gt;completion_events.read);<br>
+    }<br>
+    return done;<br>
+}<br>
+<br>
+static void<br>
+consumed_pending_read(struct obfs_test_socket_win32 *sock)<br>
+{<br>
+    struct io_slot *slot = &amp;sock-&gt;slot_read;<br>
+    assert(slot-&gt;status == IO_SLOT_COMPLETE);<br>
+    slot-&gt;status = IO_SLOT_DORMANT;<br>
+    slot-&gt;succeeded = false;<br>
+    ResetEvent(slot-&gt;overlapped.hEvent);<br>
+}<br>
+<br>
+static inline bool<br>
+complete_pending_write(struct obfs_test_socket_win32 *sock)<br>
+{<br>
+    bool done = complete_pending_operation(&amp;sock-&gt;slot_write);<br>
+    if (done)<br>
+    {<br>
+        SetEvent(sock-&gt;completion_events.write);<br>
+    }<br>
+    return done;<br>
+}<br>
+<br>
+static void<br>
+obfs_test_win32_request_event(openvpn_transport_socket_t handle,<br>
+                              openvpn_transport_event_set_handle_t event_set, unsigned rwflags)<br>
+{<br>
+    struct obfs_test_socket_win32 *sock = (struct obfs_test_socket_win32 *)handle;<br>
+    obfs_test_log(sock-&gt;ctx, PLOG_DEBUG, &quot;request-event: %d&quot;, rwflags);<br>
+    sock-&gt;last_rwflags = 0;<br>
+<br>
+    if (rwflags &amp; OPENVPN_TRANSPORT_EVENT_READ)<br>
+    {<br>
+        ensure_pending_read(sock);<br>
+    }<br>
+    if (rwflags)<br>
+    {<br>
+        event_set-&gt;vtab-&gt;set_event(event_set, &amp;sock-&gt;completion_events, rwflags, handle);<br>
+    }<br>
+}<br>
+<br>
+static bool<br>
+obfs_test_win32_update_event(openvpn_transport_socket_t handle, void *arg, unsigned rwflags)<br>
+{<br>
+    obfs_test_log(((struct obfs_test_socket_win32 *) handle)-&gt;ctx, PLOG_DEBUG,<br>
+                  &quot;update-event: %p, %p, %d&quot;, handle, arg, rwflags);<br>
+    if (arg != handle)<br>
+    {<br>
+        return false;<br>
+    }<br>
+    ((struct obfs_test_socket_win32 *) handle)-&gt;last_rwflags |= rwflags;<br>
+    return true;<br>
+}<br>
+<br>
+static unsigned<br>
+obfs_test_win32_pump(openvpn_transport_socket_t handle)<br>
+{<br>
+    struct obfs_test_socket_win32 *sock = (struct obfs_test_socket_win32 *)handle;<br>
+    unsigned result = 0;<br>
+<br>
+    if ((sock-&gt;last_rwflags &amp; OPENVPN_TRANSPORT_EVENT_READ) &amp;&amp; complete_pending_read(sock))<br>
+    {<br>
+        result |= OPENVPN_TRANSPORT_EVENT_READ;<br>
+    }<br>
+    if ((sock-&gt;last_rwflags &amp; OPENVPN_TRANSPORT_EVENT_WRITE)<br>
+        &amp;&amp; (sock-&gt;slot_write.status != IO_SLOT_PENDING || complete_pending_write(sock)))<br>
+    {<br>
+        result |= OPENVPN_TRANSPORT_EVENT_WRITE;<br>
+    }<br>
+<br>
+    obfs_test_log(sock-&gt;ctx, PLOG_DEBUG, &quot;pump -&gt; %d&quot;, result);<br>
+    return result;<br>
+}<br>
+<br>
+static ssize_t<br>
+obfs_test_win32_recvfrom(openvpn_transport_socket_t handle, void *buf, size_t len,<br>
+                         struct sockaddr *addr, openvpn_transport_socklen_t *addrlen)<br>
+{<br>
+    struct obfs_test_socket_win32 *sock = (struct obfs_test_socket_win32 *)handle;<br>
+    if (!complete_pending_read(sock))<br>
+    {<br>
+        WSASetLastError(WSA_IO_INCOMPLETE);<br>
+        return -1;<br>
+    }<br>
+<br>
+    if (!sock-&gt;slot_read.succeeded)<br>
+    {<br>
+        int wsa_error = sock-&gt;slot_read.wsa_error;<br>
+        consumed_pending_read(sock);<br>
+        WSASetLastError(wsa_error);<br>
+        return -1;<br>
+    }<br>
+<br>
+    /* sock-&gt;slot_read now has valid data. */<br>
+    char *working_buf = sock-&gt;slot_read.buf;<br>
+    ssize_t unmunged_len =<br>
+        obfs_test_unmunge_buf(&amp;sock-&gt;args, working_buf,<br>
+                              sock-&gt;slot_read.buf_len);<br>
+    if (unmunged_len &lt; 0)<br>
+    {<br>
+        /* Act as though this read never happened. Assume one was queued before, so it should<br>
+         * still remain queued. */<br>
+        consumed_pending_read(sock);<br>
+        ensure_pending_read(sock);<br>
+        WSASetLastError(WSA_IO_INCOMPLETE);<br>
+        return -1;<br>
+    }<br>
+<br>
+    size_t copy_len = unmunged_len;<br>
+    if (copy_len &gt; len)<br>
+    {<br>
+        copy_len = len;<br>
+    }<br>
+    memcpy(buf, sock-&gt;slot_read.buf, copy_len);<br>
+<br>
+    /* TODO: shouldn&#39;t truncate, should signal error (but this shouldn&#39;t happen for any<br>
+     * supported address families anyway). */<br>
+    openvpn_transport_socklen_t addr_copy_len = *addrlen;<br>
+    if (sock-&gt;slot_read.addr_len &lt; addr_copy_len)<br>
+    {<br>
+        addr_copy_len = sock-&gt;slot_read.addr_len;<br>
+    }<br>
+    memcpy(addr, &amp;sock-&gt;slot_read.addr, addr_copy_len);<br>
+    *addrlen = addr_copy_len;<br>
+    if (addr_copy_len &gt; 0)<br>
+    {<br>
+        obfs_test_munge_addr(addr, addr_copy_len);<br>
+    }<br>
+<br>
+    /* Reset the I/O slot before returning. */<br>
+    consumed_pending_read(sock);<br>
+    return copy_len;<br>
+}<br>
+<br>
+static ssize_t<br>
+obfs_test_win32_sendto(openvpn_transport_socket_t handle, const void *buf, size_t len,<br>
+                       const struct sockaddr *addr, openvpn_transport_socklen_t addrlen)<br>
+{<br>
+    struct obfs_test_socket_win32 *sock = (struct obfs_test_socket_win32 *)handle;<br>
+    complete_pending_write(sock);<br>
+<br>
+    if (sock-&gt;slot_write.status == IO_SLOT_PENDING)<br>
+    {<br>
+        /* This shouldn&#39;t really happen, but. */<br>
+        WSASetLastError(WSAEWOULDBLOCK);<br>
+        return -1;<br>
+    }<br>
+<br>
+    if (addrlen &gt; sock-&gt;slot_write.addr_cap)<br>
+    {<br>
+        /* Shouldn&#39;t happen. */<br>
+        WSASetLastError(WSAEFAULT);<br>
+        return -1;<br>
+    }<br>
+<br>
+    /* TODO: propagate previous write errors---what does core expect here? */<br>
+    memcpy(&amp;sock-&gt;slot_write.addr, addr, addrlen);<br>
+    sock-&gt;slot_write.addr_len = addrlen;<br>
+    if (addrlen &gt; 0)<br>
+    {<br>
+        obfs_test_munge_addr((struct sockaddr *)&amp;sock-&gt;slot_write.addr, addrlen);<br>
+    }<br>
+    resize_io_buf(&amp;sock-&gt;slot_write, obfs_test_max_munged_buf_size(len));<br>
+    sock-&gt;slot_write.buf_len =<br>
+        obfs_test_munge_buf(&amp;sock-&gt;args, sock-&gt;slot_write.buf, buf, len);<br>
+    queue_new_write(&amp;sock-&gt;slot_write);<br>
+    switch (sock-&gt;slot_write.status)<br>
+    {<br>
+        case IO_SLOT_PENDING:<br>
+            /* The network hasn&#39;t given us an error yet, but _we&#39;ve_ consumed all the bytes.<br>
+             * ... sort of. */<br>
+            return len;<br>
+<br>
+        case IO_SLOT_DORMANT:<br>
+            /* Huh?? But we just queued a write. */<br>
+            abort();<br>
+<br>
+        case IO_SLOT_COMPLETE:<br>
+            if (sock-&gt;slot_write.succeeded)<br>
+            {<br>
+                /* TODO: more partial length handling */<br>
+                return len;<br>
+            }<br>
+            else<br>
+            {<br>
+                return -1;<br>
+            }<br>
+<br>
+        default:<br>
+            abort();<br>
+    }<br>
+}<br>
+<br>
+static void<br>
+obfs_test_win32_close(openvpn_transport_socket_t handle)<br>
+{<br>
+    free_socket((struct obfs_test_socket_win32 *) handle);<br>
+}<br>
+<br>
+void<br>
+obfs_test_initialize_vtabs_platform(void)<br>
+{<br>
+    obfs_test_bind_vtab.bind = obfs_test_win32_bind;<br>
+    obfs_test_socket_vtab.request_event = obfs_test_win32_request_event;<br>
+    obfs_test_socket_vtab.update_event = obfs_test_win32_update_event;<br>
+    obfs_test_socket_vtab.pump = obfs_test_win32_pump;<br>
+    obfs_test_socket_vtab.recvfrom = obfs_test_win32_recvfrom;<br>
+    obfs_test_socket_vtab.sendto = obfs_test_win32_sendto;<br>
+    obfs_test_socket_vtab.close = obfs_test_win32_close;<br>
+}<br>
diff --git a/src/plugins/obfs-test/obfs-test.c b/src/plugins/obfs-test/obfs-test.c<br>
new file mode 100644<br>
index 00000000..27a3d21e<br>
--- /dev/null<br>
+++ b/src/plugins/obfs-test/obfs-test.c<br>
@@ -0,0 +1,94 @@<br>
+#include &lt;stdlib.h&gt;<br>
+#include &lt;string.h&gt;<br>
+#include &lt;stdbool.h&gt;<br>
+#include &quot;openvpn-plugin.h&quot;<br>
+#include &quot;openvpn-transport.h&quot;<br>
+#include &quot;obfs-test.h&quot;<br>
+<br>
+struct openvpn_transport_bind_vtab1 obfs_test_bind_vtab = { 0 };<br>
+struct openvpn_transport_socket_vtab1 obfs_test_socket_vtab = { 0 };<br>
+<br>
+struct obfs_test_context<br>
+{<br>
+    struct openvpn_plugin_callbacks *global_vtab;<br>
+};<br>
+<br>
+static void<br>
+free_context(struct obfs_test_context *context)<br>
+{<br>
+    if (!context)<br>
+    {<br>
+        return;<br>
+    }<br>
+    free(context);<br>
+}<br>
+<br>
+OPENVPN_EXPORT int<br>
+openvpn_plugin_open_v3(int version, struct openvpn_plugin_args_open_in const *args,<br>
+                       struct openvpn_plugin_args_open_return *out)<br>
+{<br>
+    struct obfs_test_context *context;<br>
+<br>
+    context = (struct obfs_test_context *) calloc(1, sizeof(struct obfs_test_context));<br>
+    if (!context)<br>
+    {<br>
+        return OPENVPN_PLUGIN_FUNC_ERROR;<br>
+    }<br>
+<br>
+    context-&gt;global_vtab = args-&gt;callbacks;<br>
+    obfs_test_initialize_vtabs_platform();<br>
+    obfs_test_bind_vtab.parseargs = obfs_test_parseargs;<br>
+    obfs_test_bind_vtab.argerror = obfs_test_argerror;<br>
+    obfs_test_bind_vtab.freeargs = obfs_test_freeargs;<br>
+<br>
+    out-&gt;type_mask = OPENVPN_PLUGIN_MASK(OPENVPN_PLUGIN_TRANSPORT);<br>
+    out-&gt;handle = (openvpn_plugin_handle_t *) context;<br>
+    return OPENVPN_PLUGIN_FUNC_SUCCESS;<br>
+<br>
+err:<br>
+    free_context(context);<br>
+    return OPENVPN_PLUGIN_FUNC_ERROR;<br>
+}<br>
+<br>
+OPENVPN_EXPORT void<br>
+openvpn_plugin_close_v1(openvpn_plugin_handle_t handle)<br>
+{<br>
+    free_context((struct obfs_test_context *) handle);<br>
+}<br>
+<br>
+OPENVPN_EXPORT int<br>
+openvpn_plugin_func_v3(int version,<br>
+                       struct openvpn_plugin_args_func_in const *arguments,<br>
+                       struct openvpn_plugin_args_func_return *retptr)<br>
+{<br>
+    /* We don&#39;t ask for any bits that use this interface. */<br>
+    return OPENVPN_PLUGIN_FUNC_ERROR;<br>
+}<br>
+<br>
+OPENVPN_EXPORT void *<br>
+openvpn_plugin_get_vtab_v1(int selector, size_t *size_out)<br>
+{<br>
+    switch (selector)<br>
+    {<br>
+        case OPENVPN_VTAB_TRANSPORT_BIND_V1:<br>
+            if (obfs_test_bind_vtab.bind == NULL)<br>
+            {<br>
+                return NULL;<br>
+            }<br>
+            *size_out = sizeof(struct openvpn_transport_bind_vtab1);<br>
+            return &amp;obfs_test_bind_vtab;<br>
+<br>
+        default:<br>
+            return NULL;<br>
+    }<br>
+}<br>
+<br>
+void<br>
+obfs_test_log(struct obfs_test_context *ctx,<br>
+              openvpn_plugin_log_flags_t flags, const char *fmt, ...)<br>
+{<br>
+    va_list va;<br>
+    va_start(va, fmt);<br>
+    ctx-&gt;global_vtab-&gt;plugin_vlog(flags, OBFS_TEST_PLUGIN_NAME, fmt, va);<br>
+    va_end(va);<br>
+}<br>
diff --git a/src/plugins/obfs-test/obfs-test.exports b/src/plugins/obfs-test/obfs-test.exports<br>
new file mode 100644<br>
index 00000000..e7baada4<br>
--- /dev/null<br>
+++ b/src/plugins/obfs-test/obfs-test.exports<br>
@@ -0,0 +1,4 @@<br>
+openvpn_plugin_open_v3<br>
+openvpn_plugin_close_v1<br>
+openvpn_plugin_get_vtab_v1<br>
+openvpn_plugin_func_v3<br>
diff --git a/src/plugins/obfs-test/obfs-test.h b/src/plugins/obfs-test/obfs-test.h<br>
new file mode 100644<br>
index 00000000..b9a6f8b4<br>
--- /dev/null<br>
+++ b/src/plugins/obfs-test/obfs-test.h<br>
@@ -0,0 +1,42 @@<br>
+#ifndef OPENVPN_PLUGIN_OBFS_TEST_H<br>
+#define OPENVPN_PLUGIN_OBFS_TEST_H 1<br>
+<br>
+#include &quot;openvpn-plugin.h&quot;<br>
+#include &quot;openvpn-transport.h&quot;<br>
+<br>
+#define OBFS_TEST_PLUGIN_NAME &quot;obfs-test&quot;<br>
+<br>
+struct obfs_test_context;<br>
+<br>
+struct obfs_test_args<br>
+{<br>
+    const char *error;<br>
+    int offset;<br>
+};<br>
+<br>
+extern struct openvpn_transport_bind_vtab1 obfs_test_bind_vtab;<br>
+extern struct openvpn_transport_socket_vtab1 obfs_test_socket_vtab;<br>
+<br>
+void obfs_test_initialize_vtabs_platform(void);<br>
+<br>
+void obfs_test_munge_addr(struct sockaddr *addr, openvpn_transport_socklen_t len);<br>
+<br>
+size_t obfs_test_max_munged_buf_size(size_t clear_size);<br>
+<br>
+size_t obfs_test_munge_buf(struct obfs_test_args *how,<br>
+                           char *out, const char *in, size_t len);<br>
+<br>
+ssize_t obfs_test_unmunge_buf(struct obfs_test_args *how,<br>
+                              char *buf, size_t len);<br>
+<br>
+openvpn_transport_args_t obfs_test_parseargs(void *plugin_handle,<br>
+                                             const char *const *argv, int argc);<br>
+<br>
+const char *obfs_test_argerror(openvpn_transport_args_t args);<br>
+<br>
+void obfs_test_freeargs(openvpn_transport_args_t args);<br>
+<br>
+void obfs_test_log(struct obfs_test_context *ctx,<br>
+                   openvpn_plugin_log_flags_t flags, const char *fmt, ...);<br>
+<br>
+#endif /* !OPENVPN_PLUGIN_OBFS_TEST_H */<br>
-- <br>
2.19.2<br>
<br>
<br>
<br>
_______________________________________________<br>
Openvpn-devel mailing list<br>
<a href="mailto:Openvpn-devel@lists.sourceforge.net" target="_blank">Openvpn-devel@lists.sourceforge.net</a><br>
<a href="https://lists.sourceforge.net/lists/listinfo/openvpn-devel" rel="noreferrer" target="_blank">https://lists.sourceforge.net/lists/listinfo/openvpn-devel</a><br>
</blockquote></div>

Patch

diff --git a/configure.ac b/configure.ac
index 1e6891b1..b4196812 100644
--- a/configure.ac
+++ b/configure.ac
@@ -200,6 +200,13 @@  AC_ARG_ENABLE(
 	]
 )
 
+AC_ARG_ENABLE(
+	[plugin-obfs-test],
+	[AS_HELP_STRING([--disable-plugin-obfs-test], [disable obfs-test plugin @<:@default=platform specific@:>@])],
+	,
+	[enable_plugin_obfs_test="no"]
+)
+
 AC_ARG_ENABLE(
 	[pam-dlopen],
 	[AS_HELP_STRING([--enable-pam-dlopen], [dlopen libpam @<:@default=no@:>@])],
@@ -1344,6 +1351,7 @@  AM_CONDITIONAL([WIN32], [test "${WIN32}" = "yes"])
 AM_CONDITIONAL([GIT_CHECKOUT], [test "${GIT_CHECKOUT}" = "yes"])
 AM_CONDITIONAL([ENABLE_PLUGIN_AUTH_PAM], [test "${enable_plugin_auth_pam}" = "yes"])
 AM_CONDITIONAL([ENABLE_PLUGIN_DOWN_ROOT], [test "${enable_plugin_down_root}" = "yes"])
+AM_CONDITIONAL([ENABLE_PLUGIN_OBFS_TEST], [test "${enable_plugin_obfs_test}" = "yes"])
 AM_CONDITIONAL([HAVE_LD_WRAP_SUPPORT], [test "${have_ld_wrap_support}" = "yes"])
 
 sampledir="\$(docdir)/sample"
@@ -1403,6 +1411,7 @@  AC_CONFIG_FILES([
 	src/plugins/Makefile
 	src/plugins/auth-pam/Makefile
 	src/plugins/down-root/Makefile
+	src/plugins/obfs-test/Makefile
 	tests/Makefile
         tests/unit_tests/Makefile
         tests/unit_tests/example_test/Makefile
diff --git a/src/plugins/Makefile.am b/src/plugins/Makefile.am
index f3461786..848bac03 100644
--- a/src/plugins/Makefile.am
+++ b/src/plugins/Makefile.am
@@ -12,4 +12,4 @@ 
 MAINTAINERCLEANFILES = \
 	$(srcdir)/Makefile.in
 
-SUBDIRS = auth-pam down-root
+SUBDIRS = auth-pam down-root obfs-test
diff --git a/src/plugins/obfs-test/Makefile.am b/src/plugins/obfs-test/Makefile.am
new file mode 100644
index 00000000..4cc8d183
--- /dev/null
+++ b/src/plugins/obfs-test/Makefile.am
@@ -0,0 +1,29 @@ 
+MAINTAINERCLEANFILES = \
+	$(srcdir)/Makefile.in
+
+AM_CFLAGS = \
+	-I$(top_srcdir)/include \
+	$(OPTIONAL_CRYPTO_CFLAGS)
+
+if ENABLE_PLUGIN_OBFS_TEST
+plugin_LTLIBRARIES = openvpn-plugin-obfs-test.la
+endif
+
+openvpn_plugin_obfs_test_la_SOURCES = \
+	obfs-test.c \
+	obfs-test-munging.c \
+	obfs-test-args.c \
+	obfs-test.exports
+
+if WIN32
+openvpn_plugin_obfs_test_la_SOURCES += obfs-test-win32.c
+openvpn_plugin_obfs_test_la_LIBADD = -lws2_32 -lwininet
+else !WIN32
+openvpn_plugin_obfs_test_la_SOURCES += obfs-test-posix.c
+# No LIBADD necessary; we assume we can access the global symbol space,
+# and core OpenVPN will already link with everything needed for sockets.
+endif
+
+openvpn_plugin_obfs_test_la_LDFLAGS = $(AM_LDFLAGS) \
+	-export-symbols "$(srcdir)/obfs-test.exports" \
+	-module -shared -avoid-version -no-undefined
diff --git a/src/plugins/obfs-test/README.obfs-test b/src/plugins/obfs-test/README.obfs-test
new file mode 100644
index 00000000..5492ee02
--- /dev/null
+++ b/src/plugins/obfs-test/README.obfs-test
@@ -0,0 +1,26 @@ 
+obfs-test
+
+SYNOPSIS
+
+The obfs-test plugin is a proof of concept for supporting protocol
+obfuscation for OpenVPN via a socket intercept plugin.
+
+BUILD
+
+You must specify --enable-plugin-obfs-test at configure time to
+trigger building this plugin. It should function on POSIX-y platforms
+and Windows.
+
+USAGE
+
+To invoke this plugin, load it via an appropriate plugin line in the
+configuration file, and then specify 'proto indirect' rather than any
+other protocol. Packets will then be passed via UDP, but they will
+also undergo a very basic content transformation, and the bind port
+will be altered (see obfs-test-munging.c for details).
+
+CAVEATS
+
+This has undergone basic functionality testing, but not any kind of
+full-on stress test. Extended socket or I/O handling options are not
+supported at all.
diff --git a/src/plugins/obfs-test/obfs-test-args.c b/src/plugins/obfs-test/obfs-test-args.c
new file mode 100644
index 00000000..e6756f8f
--- /dev/null
+++ b/src/plugins/obfs-test/obfs-test-args.c
@@ -0,0 +1,60 @@ 
+#include "obfs-test.h"
+
+openvpn_transport_args_t
+obfs_test_parseargs(void *plugin_handle,
+                    const char *const *argv, int argc)
+{
+    struct obfs_test_args *args = calloc(1, sizeof(struct obfs_test_args));
+    if (!args)
+    {
+        return NULL;
+    }
+
+    if (argc < 2)
+    {
+        args->offset = 0;
+    }
+    else if (argc == 2)
+    {
+        char *end;
+        long offset = strtol(argv[1], &end, 10);
+        if (*end != '\0')
+        {
+            args->error = "offset must be a decimal number";
+        }
+        else if (!(0 <= offset && offset <= 42))
+        {
+            args->error = "offset must be between 0 and 42";
+        }
+        else
+        {
+            args->offset = (int) offset;
+        }
+    }
+    else
+    {
+        args->error = "too many arguments";
+    }
+
+    return args;
+}
+
+const char *
+obfs_test_argerror(openvpn_transport_args_t args_)
+{
+    if (!args_)
+    {
+        return "cannot allocate";
+    }
+    else
+    {
+        return ((struct obfs_test_args *) args_)->error;
+    }
+}
+
+void
+obfs_test_freeargs(openvpn_transport_args_t args_)
+{
+    free(args_);
+    struct obfs_test_args *args = (struct obfs_test_args *) args_;
+}
diff --git a/src/plugins/obfs-test/obfs-test-munging.c b/src/plugins/obfs-test/obfs-test-munging.c
new file mode 100644
index 00000000..37d27039
--- /dev/null
+++ b/src/plugins/obfs-test/obfs-test-munging.c
@@ -0,0 +1,129 @@ 
+#include <string.h>
+#include <errno.h>
+#include <stdbool.h>
+#include "obfs-test.h"
+#ifdef OPENVPN_TRANSPORT_PLATFORM_POSIX
+#include <sys/socket.h>
+#include <netinet/in.h>
+typedef in_port_t obfs_test_in_port_t;
+#else
+#include <winsock2.h>
+#include <ws2tcpip.h>
+typedef u_short obfs_test_in_port_t;
+#endif
+
+static obfs_test_in_port_t
+munge_port(obfs_test_in_port_t port)
+{
+    return port ^ 15;
+}
+
+/* Reversible. */
+void
+obfs_test_munge_addr(struct sockaddr *addr, openvpn_transport_socklen_t len)
+{
+    struct sockaddr_in *inet;
+    struct sockaddr_in6 *inet6;
+
+    switch (addr->sa_family)
+    {
+        case AF_INET:
+            inet = (struct sockaddr_in *) addr;
+            inet->sin_port = munge_port(inet->sin_port);
+            break;
+
+        case AF_INET6:
+            inet6 = (struct sockaddr_in6 *) addr;
+            inet6->sin6_port = munge_port(inet6->sin6_port);
+            break;
+
+        default:
+            break;
+    }
+}
+
+/* Six fixed bytes, six repeated bytes. It's only a silly transformation. */
+#define MUNGE_OVERHEAD 12
+
+size_t
+obfs_test_max_munged_buf_size(size_t clear_size)
+{
+    return clear_size + MUNGE_OVERHEAD;
+}
+
+ssize_t
+obfs_test_unmunge_buf(struct obfs_test_args *how,
+                      char *buf, size_t len)
+{
+    int i;
+
+    if (len < 6)
+    {
+        goto bad;
+    }
+    for (i = 0; i < 6; i++)
+    {
+        if (buf[i] != i + how->offset)
+        {
+            goto bad;
+        }
+    }
+
+    for (i = 0; i < 6 && (6 + 2*i) < len; i++)
+    {
+        if (len < (6 + 2*i + 1) || buf[6 + 2*i] != buf[6 + 2*i + 1])
+        {
+            goto bad;
+        }
+        buf[i] = buf[6 + 2*i];
+    }
+
+    if (len > 18)
+    {
+        memmove(buf + 6, buf + 18, len - 18);
+        len -= 12;
+    }
+    else
+    {
+        len -= 6;
+        len /= 2;
+    }
+
+    return len;
+
+bad:
+    /* TODO: this really isn't the best way to report this error */
+    errno = EIO;
+    return -1;
+}
+
+/* out must have space for len+MUNGE_OVERHEAD bytes. out and in must
+ * not overlap. */
+size_t
+obfs_test_munge_buf(struct obfs_test_args *how,
+                    char *out, const char *in, size_t len)
+{
+    int i, n;
+    size_t out_len = 6;
+
+    for (i = 0; i < 6; i++)
+    {
+        out[i] = i + how->offset;
+    }
+    n = len < 6 ? len : 6;
+    for (i = 0; i < n; i++)
+    {
+        out[6 + 2*i] = out[6 + 2*i + 1] = in[i];
+    }
+    if (len > 6)
+    {
+        memmove(out + 18, in + 6, len - 6);
+        out_len = len + 12;
+    }
+    else
+    {
+        out_len = 6 + 2*len;
+    }
+
+    return out_len;
+}
diff --git a/src/plugins/obfs-test/obfs-test-posix.c b/src/plugins/obfs-test/obfs-test-posix.c
new file mode 100644
index 00000000..826381c5
--- /dev/null
+++ b/src/plugins/obfs-test/obfs-test-posix.c
@@ -0,0 +1,207 @@ 
+#include "obfs-test.h"
+#include <stdbool.h>
+#include <string.h>
+#include <err.h>
+#include <errno.h>
+#include <unistd.h>
+#include <fcntl.h>
+#include <sys/socket.h>
+#include <netinet/in.h>
+
+struct obfs_test_socket_posix
+{
+    struct openvpn_transport_socket handle;
+    struct obfs_test_args args;
+    struct obfs_test_context *ctx;
+    int fd;
+    unsigned last_rwflags;
+};
+
+static void
+free_socket(struct obfs_test_socket_posix *sock)
+{
+    if (!sock)
+    {
+        return;
+    }
+    if (sock->fd != -1)
+    {
+        close(sock->fd);
+    }
+    free(sock);
+}
+
+static openvpn_transport_socket_t
+obfs_test_posix_bind(void *plugin_handle, openvpn_transport_args_t args,
+                     const struct sockaddr *addr, socklen_t len)
+{
+    struct obfs_test_socket_posix *sock = NULL;
+    struct sockaddr *addr_rev = NULL;
+
+    addr_rev = calloc(1, len);
+    if (!addr_rev)
+    {
+        goto error;
+    }
+    memcpy(addr_rev, addr, len);
+    obfs_test_munge_addr(addr_rev, len);
+
+    sock = calloc(1, sizeof(struct obfs_test_socket_posix));
+    if (!sock)
+    {
+        goto error;
+    }
+    sock->handle.vtab = &obfs_test_socket_vtab;
+    sock->ctx = (struct obfs_test_context *) plugin_handle;
+    memcpy(&sock->args, args, sizeof(sock->args));
+    /* Note that sock->fd isn't -1 yet. Set it explicitly if there are ever any
+     * error exits before the socket() call. */
+
+    sock->fd = socket(addr->sa_family, SOCK_DGRAM, IPPROTO_UDP);
+    if (sock->fd == -1)
+    {
+        goto error;
+    }
+    if (fcntl(sock->fd, F_SETFL, fcntl(sock->fd, F_GETFL) | O_NONBLOCK))
+    {
+        goto error;
+    }
+
+    if (bind(sock->fd, addr_rev, len))
+    {
+        goto error;
+    }
+    free(addr_rev);
+    return &sock->handle;
+
+error:
+    free_socket(sock);
+    free(addr_rev);
+    return NULL;
+}
+
+static void
+obfs_test_posix_request_event(openvpn_transport_socket_t handle,
+                              openvpn_transport_event_set_handle_t event_set, unsigned rwflags)
+{
+    obfs_test_log(((struct obfs_test_socket_posix *) handle)->ctx,
+                  PLOG_DEBUG, "request-event: %d", rwflags);
+    ((struct obfs_test_socket_posix *) handle)->last_rwflags = 0;
+    if (rwflags)
+    {
+        event_set->vtab->set_event(event_set, ((struct obfs_test_socket_posix *) handle)->fd,
+                                   rwflags, handle);
+    }
+}
+
+static bool
+obfs_test_posix_update_event(openvpn_transport_socket_t handle, void *arg, unsigned rwflags)
+{
+    obfs_test_log(((struct obfs_test_socket_posix *) handle)->ctx,
+                  PLOG_DEBUG, "update-event: %p, %p, %d", handle, arg, rwflags);
+    if (arg != handle)
+    {
+        return false;
+    }
+    ((struct obfs_test_socket_posix *) handle)->last_rwflags |= rwflags;
+    return true;
+}
+
+static unsigned
+obfs_test_posix_pump(openvpn_transport_socket_t handle)
+{
+    obfs_test_log(((struct obfs_test_socket_posix *) handle)->ctx,
+                  PLOG_DEBUG, "pump -> %d", ((struct obfs_test_socket_posix *) handle)->last_rwflags);
+    return ((struct obfs_test_socket_posix *) handle)->last_rwflags;
+}
+
+static ssize_t
+obfs_test_posix_recvfrom(openvpn_transport_socket_t handle, void *buf, size_t len,
+                         struct sockaddr *addr, socklen_t *addrlen)
+{
+    int fd = ((struct obfs_test_socket_posix *) handle)->fd;
+    ssize_t result;
+
+again:
+    result = recvfrom(fd, buf, len, 0, addr, addrlen);
+    if (result < 0 && errno == EAGAIN)
+    {
+        ((struct obfs_test_socket_posix *) handle)->last_rwflags &= ~OPENVPN_TRANSPORT_EVENT_READ;
+    }
+    if (*addrlen > 0)
+    {
+        obfs_test_munge_addr(addr, *addrlen);
+    }
+    if (result > 0)
+    {
+        struct obfs_test_args *how = &((struct obfs_test_socket_posix *) handle)->args;
+        result = obfs_test_unmunge_buf(how, buf, result);
+        if (result < 0)
+        {
+            /* Pretend that read never happened. */
+            goto again;
+        }
+    }
+
+    obfs_test_log(((struct obfs_test_socket_posix *) handle)->ctx,
+                  PLOG_DEBUG, "recvfrom(%d) -> %d", (int)len, (int)result);
+    return result;
+}
+
+static ssize_t
+obfs_test_posix_sendto(openvpn_transport_socket_t handle, const void *buf, size_t len,
+                       const struct sockaddr *addr, socklen_t addrlen)
+{
+    int fd = ((struct obfs_test_socket_posix *) handle)->fd;
+    struct sockaddr *addr_rev = calloc(1, addrlen);
+    void *buf_munged = malloc(obfs_test_max_munged_buf_size(len));
+    size_t len_munged;
+    ssize_t result;
+    if (!addr_rev || !buf_munged)
+    {
+        goto error;
+    }
+
+    memcpy(addr_rev, addr, addrlen);
+    obfs_test_munge_addr(addr_rev, addrlen);
+    struct obfs_test_args *how = &((struct obfs_test_socket_posix *) handle)->args;
+    len_munged = obfs_test_munge_buf(how, buf_munged, buf, len);
+    result = sendto(fd, buf_munged, len_munged, 0, addr_rev, addrlen);
+    if (result < 0 && errno == EAGAIN)
+    {
+        ((struct obfs_test_socket_posix *) handle)->last_rwflags &= ~OPENVPN_TRANSPORT_EVENT_WRITE;
+    }
+    /* TODO: not clear what to do here for partial transfers. */
+    if (result > len)
+    {
+        result = len;
+    }
+    obfs_test_log(((struct obfs_test_socket_posix *) handle)->ctx,
+                  PLOG_DEBUG, "sendto(%d) -> %d", (int)len, (int)result);
+    free(addr_rev);
+    free(buf_munged);
+    return result;
+
+error:
+    free(addr_rev);
+    free(buf_munged);
+    return -1;
+}
+
+static void
+obfs_test_posix_close(openvpn_transport_socket_t handle)
+{
+    free_socket((struct obfs_test_socket_posix *) handle);
+}
+
+void
+obfs_test_initialize_vtabs_platform(void)
+{
+    obfs_test_bind_vtab.bind = obfs_test_posix_bind;
+    obfs_test_socket_vtab.request_event = obfs_test_posix_request_event;
+    obfs_test_socket_vtab.update_event = obfs_test_posix_update_event;
+    obfs_test_socket_vtab.pump = obfs_test_posix_pump;
+    obfs_test_socket_vtab.recvfrom = obfs_test_posix_recvfrom;
+    obfs_test_socket_vtab.sendto = obfs_test_posix_sendto;
+    obfs_test_socket_vtab.close = obfs_test_posix_close;
+}
diff --git a/src/plugins/obfs-test/obfs-test-win32.c b/src/plugins/obfs-test/obfs-test-win32.c
new file mode 100644
index 00000000..46c95f55
--- /dev/null
+++ b/src/plugins/obfs-test/obfs-test-win32.c
@@ -0,0 +1,579 @@ 
+#include "obfs-test.h"
+#include <stdbool.h>
+#include <string.h>
+#include <stdio.h>
+#include <stdarg.h>
+#include <windows.h>
+#include <winsock2.h>
+#include <assert.h>
+
+static inline bool
+is_invalid_handle(HANDLE h)
+{
+    return h == NULL || h == INVALID_HANDLE_VALUE;
+}
+
+typedef enum {
+    IO_SLOT_DORMANT,            /* must be 0 for calloc purposes */
+    IO_SLOT_PENDING,
+    /* success/failure is determined by succeeded flag in COMPLETE state */
+    IO_SLOT_COMPLETE
+} io_slot_status_t;
+
+/* must be calloc'able */
+struct io_slot
+{
+    struct obfs_test_context *ctx;
+    io_slot_status_t status;
+    OVERLAPPED overlapped;
+    SOCKET socket;
+    SOCKADDR_STORAGE addr;
+    int addr_len, addr_cap;
+    DWORD bytes, flags;
+    bool succeeded;
+    int wsa_error;
+
+    /* realloc'd as needed; always private copy, never aliased */
+    char *buf;
+    size_t buf_len, buf_cap;
+};
+
+static bool
+setup_io_slot(struct io_slot *slot, struct obfs_test_context *ctx,
+              SOCKET socket, HANDLE event)
+{
+    slot->ctx = ctx;
+    slot->status = IO_SLOT_DORMANT;
+    slot->addr_cap = sizeof(SOCKADDR_STORAGE);
+    slot->socket = socket;
+    slot->overlapped.hEvent = event;
+    return true;
+}
+
+/* Note that this assumes any I/O has already been implicitly canceled (via closesocket),
+ * but not waited for yet. */
+static bool
+destroy_io_slot(struct io_slot *slot)
+{
+    if (slot->status == IO_SLOT_PENDING)
+    {
+        DWORD bytes, flags;
+        BOOL ok = WSAGetOverlappedResult(slot->socket, &slot->overlapped, &bytes,
+                                         TRUE /* wait */, &flags);
+        if (!ok && WSAGetLastError() == WSA_IO_INCOMPLETE)
+        {
+            obfs_test_log(slot->ctx, PLOG_ERR,
+                          "destroying I/O slot: canceled operation is still incomplete after wait?!");
+            return false;
+        }
+    }
+
+    slot->status = IO_SLOT_DORMANT;
+    return true;
+}
+
+/* FIXME: aborts on error. */
+static void
+resize_io_buf(struct io_slot *slot, size_t cap)
+{
+    if (slot->buf)
+    {
+        free(slot->buf);
+        slot->buf = NULL;
+    }
+
+    char *new_buf = malloc(cap);
+    if (!new_buf)
+    {
+        abort();
+    }
+    slot->buf = new_buf;
+    slot->buf_cap = cap;
+}
+
+struct obfs_test_socket_win32
+{
+    struct openvpn_transport_socket handle;
+    struct obfs_test_args args;
+    struct obfs_test_context *ctx;
+    SOCKET socket;
+
+    /* Write is ready when idle; read is not-ready when idle. Both level-triggered. */
+    struct openvpn_transport_win32_event_pair completion_events;
+    struct io_slot slot_read, slot_write;
+
+    int last_rwflags;
+};
+
+static void
+free_socket(struct obfs_test_socket_win32 *sock)
+{
+    /* This only ever becomes false in strange situations where we leak the entire structure for
+     * lack of anything else to do. */
+    bool can_free = true;
+
+    if (!sock)
+    {
+        return;
+    }
+    if (sock->socket != INVALID_SOCKET)
+    {
+        closesocket(sock->socket);
+    }
+
+    /* closesocket cancels any pending overlapped I/O, but we still have to potentially
+     * wait for it here before we can free the buffers. This has to happen before closing
+     * the event handles.
+     *
+     * If we can't figure out when the canceled overlapped I/O is done, for any reason, we defensively
+     * leak the entire structure; freeing it would be permitting the system to corrupt memory later.
+     * TODO: possibly abort() instead, but make sure we've handled all the possible "have to try again"
+     * cases above first
+     */
+    if (!destroy_io_slot(&sock->slot_read))
+    {
+        can_free = false;
+    }
+    if (!destroy_io_slot(&sock->slot_write))
+    {
+        can_free = false;
+    }
+    if (!can_free)
+    {
+        /* Skip deinitialization of everything else. Doomed. */
+        obfs_test_log(sock->ctx, PLOG_ERR, "doomed, leaking the entire socket structure");
+        return;
+    }
+
+    if (!is_invalid_handle(sock->completion_events.read))
+    {
+        CloseHandle(sock->completion_events.read);
+    }
+    if (!is_invalid_handle(sock->completion_events.write))
+    {
+        CloseHandle(sock->completion_events.write);
+    }
+
+    free(sock);
+}
+
+static openvpn_transport_socket_t
+obfs_test_win32_bind(void *plugin_handle, openvpn_transport_args_t args,
+                     const struct sockaddr *addr, openvpn_transport_socklen_t len)
+{
+    struct obfs_test_socket_win32 *sock = NULL;
+    struct sockaddr *addr_rev = NULL;
+
+    /* TODO: would be nice to factor out some of these sequences */
+    addr_rev = calloc(1, len);
+    if (!addr_rev)
+    {
+        goto error;
+    }
+    memcpy(addr_rev, addr, len);
+    obfs_test_munge_addr(addr_rev, len);
+
+    sock = calloc(1, sizeof(struct obfs_test_socket_win32));
+    if (!sock)
+    {
+        goto error;
+    }
+    sock->handle.vtab = &obfs_test_socket_vtab;
+    sock->ctx = (struct obfs_test_context *) plugin_handle;
+    memcpy(&sock->args, args, sizeof(sock->args));
+
+    /* Preemptively initialize the members of some Win32 types so error exits are okay later on.
+     * HANDLEs of NULL are considered invalid per above. */
+    sock->socket = INVALID_SOCKET;
+
+    sock->socket = socket(addr_rev->sa_family, SOCK_DGRAM, IPPROTO_UDP);
+    if (sock->socket == INVALID_SOCKET)
+    {
+        goto error;
+    }
+
+    /* See above: write is ready when idle, read is not-ready when idle. */
+    sock->completion_events.read = CreateEvent(NULL, TRUE, FALSE, NULL);
+    sock->completion_events.write = CreateEvent(NULL, TRUE, TRUE, NULL);
+    if (is_invalid_handle(sock->completion_events.read) || is_invalid_handle(sock->completion_events.write))
+    {
+        goto error;
+    }
+    if (!setup_io_slot(&sock->slot_read, sock->ctx,
+                       sock->socket, sock->completion_events.read))
+    {
+        goto error;
+    }
+    if (!setup_io_slot(&sock->slot_write, sock->ctx,
+                       sock->socket, sock->completion_events.write))
+    {
+        goto error;
+    }
+
+    if (bind(sock->socket, addr_rev, len))
+    {
+        goto error;
+    }
+    free(addr_rev);
+    return &sock->handle;
+
+error:
+    obfs_test_log((struct obfs_test_context *) plugin_handle, PLOG_ERR,
+                  "bind failure: WSA error = %d", WSAGetLastError());
+    free_socket(sock);
+    free(addr_rev);
+    return NULL;
+}
+
+static void
+handle_sendrecv_return(struct io_slot *slot, int status)
+{
+    if (status == 0)
+    {
+        /* Immediately completed. Set the event so it stays consistent. */
+        slot->status = IO_SLOT_COMPLETE;
+        slot->succeeded = true;
+        slot->buf_len = slot->bytes;
+        SetEvent(slot->overlapped.hEvent);
+    }
+    else if (WSAGetLastError() == WSA_IO_PENDING)
+    {
+        /* Queued. */
+        slot->status = IO_SLOT_PENDING;
+    }
+    else
+    {
+        /* Error. */
+        slot->status = IO_SLOT_COMPLETE;
+        slot->succeeded = false;
+        slot->wsa_error = WSAGetLastError();
+        slot->buf_len = 0;
+    }
+}
+
+static void
+queue_new_read(struct io_slot *slot, size_t cap)
+{
+    int status;
+    WSABUF sbuf;
+    assert(slot->status == IO_SLOT_DORMANT);
+
+    ResetEvent(slot->overlapped.hEvent);
+    resize_io_buf(slot, cap);
+    sbuf.buf = slot->buf;
+    sbuf.len = slot->buf_cap;
+    slot->addr_len = slot->addr_cap;
+    slot->flags = 0;
+    status = WSARecvFrom(slot->socket, &sbuf, 1, &slot->bytes, &slot->flags,
+                         (struct sockaddr *)&slot->addr, &slot->addr_len,
+                         &slot->overlapped, NULL);
+    handle_sendrecv_return(slot, status);
+}
+
+/* write slot buffer must already be full. */
+static void
+queue_new_write(struct io_slot *slot)
+{
+    int status;
+    WSABUF sbuf;
+    assert(slot->status == IO_SLOT_COMPLETE || slot->status == IO_SLOT_DORMANT);
+
+    ResetEvent(slot->overlapped.hEvent);
+    sbuf.buf = slot->buf;
+    sbuf.len = slot->buf_len;
+    slot->flags = 0;
+    status = WSASendTo(slot->socket, &sbuf, 1, &slot->bytes, 0 /* flags */,
+                       (struct sockaddr *)&slot->addr, slot->addr_len,
+                       &slot->overlapped, NULL);
+    handle_sendrecv_return(slot, status);
+}
+
+static void
+ensure_pending_read(struct obfs_test_socket_win32 *sock)
+{
+    struct io_slot *slot = &sock->slot_read;
+    switch (slot->status)
+    {
+        case IO_SLOT_PENDING:
+            return;
+
+        case IO_SLOT_COMPLETE:
+            /* Set the event manually here just in case. */
+            SetEvent(slot->overlapped.hEvent);
+            return;
+
+        case IO_SLOT_DORMANT:
+            /* TODO: we don't propagate max read size here, so we just have to assume the maximum. */
+            queue_new_read(slot, 65536);
+            return;
+
+        default:
+            abort();
+    }
+}
+
+static bool
+complete_pending_operation(struct io_slot *slot)
+{
+    DWORD bytes, flags;
+    BOOL ok;
+
+    switch (slot->status)
+    {
+        case IO_SLOT_DORMANT:
+            /* TODO: shouldn't get here? */
+            return false;
+
+        case IO_SLOT_COMPLETE:
+            return true;
+
+        case IO_SLOT_PENDING:
+            ok = WSAGetOverlappedResult(slot->socket, &slot->overlapped, &bytes,
+                                        FALSE /* don't wait */, &flags);
+            if (!ok && WSAGetLastError() == WSA_IO_INCOMPLETE)
+            {
+                /* Still waiting. */
+                return false;
+            }
+            else if (ok)
+            {
+                /* Completed. slot->addr_len has already been updated. */
+                slot->buf_len = bytes;
+                slot->status = IO_SLOT_COMPLETE;
+                slot->succeeded = true;
+                return true;
+            }
+            else
+            {
+                /* Error. */
+                slot->buf_len = 0;
+                slot->status = IO_SLOT_COMPLETE;
+                slot->succeeded = false;
+                slot->wsa_error = WSAGetLastError();
+                return true;
+            }
+
+        default:
+            abort();
+    }
+}
+
+static bool
+complete_pending_read(struct obfs_test_socket_win32 *sock)
+{
+    bool done = complete_pending_operation(&sock->slot_read);
+    if (done)
+    {
+        ResetEvent(sock->completion_events.read);
+    }
+    return done;
+}
+
+static void
+consumed_pending_read(struct obfs_test_socket_win32 *sock)
+{
+    struct io_slot *slot = &sock->slot_read;
+    assert(slot->status == IO_SLOT_COMPLETE);
+    slot->status = IO_SLOT_DORMANT;
+    slot->succeeded = false;
+    ResetEvent(slot->overlapped.hEvent);
+}
+
+static inline bool
+complete_pending_write(struct obfs_test_socket_win32 *sock)
+{
+    bool done = complete_pending_operation(&sock->slot_write);
+    if (done)
+    {
+        SetEvent(sock->completion_events.write);
+    }
+    return done;
+}
+
+static void
+obfs_test_win32_request_event(openvpn_transport_socket_t handle,
+                              openvpn_transport_event_set_handle_t event_set, unsigned rwflags)
+{
+    struct obfs_test_socket_win32 *sock = (struct obfs_test_socket_win32 *)handle;
+    obfs_test_log(sock->ctx, PLOG_DEBUG, "request-event: %d", rwflags);
+    sock->last_rwflags = 0;
+
+    if (rwflags & OPENVPN_TRANSPORT_EVENT_READ)
+    {
+        ensure_pending_read(sock);
+    }
+    if (rwflags)
+    {
+        event_set->vtab->set_event(event_set, &sock->completion_events, rwflags, handle);
+    }
+}
+
+static bool
+obfs_test_win32_update_event(openvpn_transport_socket_t handle, void *arg, unsigned rwflags)
+{
+    obfs_test_log(((struct obfs_test_socket_win32 *) handle)->ctx, PLOG_DEBUG,
+                  "update-event: %p, %p, %d", handle, arg, rwflags);
+    if (arg != handle)
+    {
+        return false;
+    }
+    ((struct obfs_test_socket_win32 *) handle)->last_rwflags |= rwflags;
+    return true;
+}
+
+static unsigned
+obfs_test_win32_pump(openvpn_transport_socket_t handle)
+{
+    struct obfs_test_socket_win32 *sock = (struct obfs_test_socket_win32 *)handle;
+    unsigned result = 0;
+
+    if ((sock->last_rwflags & OPENVPN_TRANSPORT_EVENT_READ) && complete_pending_read(sock))
+    {
+        result |= OPENVPN_TRANSPORT_EVENT_READ;
+    }
+    if ((sock->last_rwflags & OPENVPN_TRANSPORT_EVENT_WRITE)
+        && (sock->slot_write.status != IO_SLOT_PENDING || complete_pending_write(sock)))
+    {
+        result |= OPENVPN_TRANSPORT_EVENT_WRITE;
+    }
+
+    obfs_test_log(sock->ctx, PLOG_DEBUG, "pump -> %d", result);
+    return result;
+}
+
+static ssize_t
+obfs_test_win32_recvfrom(openvpn_transport_socket_t handle, void *buf, size_t len,
+                         struct sockaddr *addr, openvpn_transport_socklen_t *addrlen)
+{
+    struct obfs_test_socket_win32 *sock = (struct obfs_test_socket_win32 *)handle;
+    if (!complete_pending_read(sock))
+    {
+        WSASetLastError(WSA_IO_INCOMPLETE);
+        return -1;
+    }
+
+    if (!sock->slot_read.succeeded)
+    {
+        int wsa_error = sock->slot_read.wsa_error;
+        consumed_pending_read(sock);
+        WSASetLastError(wsa_error);
+        return -1;
+    }
+
+    /* sock->slot_read now has valid data. */
+    char *working_buf = sock->slot_read.buf;
+    ssize_t unmunged_len =
+        obfs_test_unmunge_buf(&sock->args, working_buf,
+                              sock->slot_read.buf_len);
+    if (unmunged_len < 0)
+    {
+        /* Act as though this read never happened. Assume one was queued before, so it should
+         * still remain queued. */
+        consumed_pending_read(sock);
+        ensure_pending_read(sock);
+        WSASetLastError(WSA_IO_INCOMPLETE);
+        return -1;
+    }
+
+    size_t copy_len = unmunged_len;
+    if (copy_len > len)
+    {
+        copy_len = len;
+    }
+    memcpy(buf, sock->slot_read.buf, copy_len);
+
+    /* TODO: shouldn't truncate, should signal error (but this shouldn't happen for any
+     * supported address families anyway). */
+    openvpn_transport_socklen_t addr_copy_len = *addrlen;
+    if (sock->slot_read.addr_len < addr_copy_len)
+    {
+        addr_copy_len = sock->slot_read.addr_len;
+    }
+    memcpy(addr, &sock->slot_read.addr, addr_copy_len);
+    *addrlen = addr_copy_len;
+    if (addr_copy_len > 0)
+    {
+        obfs_test_munge_addr(addr, addr_copy_len);
+    }
+
+    /* Reset the I/O slot before returning. */
+    consumed_pending_read(sock);
+    return copy_len;
+}
+
+static ssize_t
+obfs_test_win32_sendto(openvpn_transport_socket_t handle, const void *buf, size_t len,
+                       const struct sockaddr *addr, openvpn_transport_socklen_t addrlen)
+{
+    struct obfs_test_socket_win32 *sock = (struct obfs_test_socket_win32 *)handle;
+    complete_pending_write(sock);
+
+    if (sock->slot_write.status == IO_SLOT_PENDING)
+    {
+        /* This shouldn't really happen, but. */
+        WSASetLastError(WSAEWOULDBLOCK);
+        return -1;
+    }
+
+    if (addrlen > sock->slot_write.addr_cap)
+    {
+        /* Shouldn't happen. */
+        WSASetLastError(WSAEFAULT);
+        return -1;
+    }
+
+    /* TODO: propagate previous write errors---what does core expect here? */
+    memcpy(&sock->slot_write.addr, addr, addrlen);
+    sock->slot_write.addr_len = addrlen;
+    if (addrlen > 0)
+    {
+        obfs_test_munge_addr((struct sockaddr *)&sock->slot_write.addr, addrlen);
+    }
+    resize_io_buf(&sock->slot_write, obfs_test_max_munged_buf_size(len));
+    sock->slot_write.buf_len =
+        obfs_test_munge_buf(&sock->args, sock->slot_write.buf, buf, len);
+    queue_new_write(&sock->slot_write);
+    switch (sock->slot_write.status)
+    {
+        case IO_SLOT_PENDING:
+            /* The network hasn't given us an error yet, but _we've_ consumed all the bytes.
+             * ... sort of. */
+            return len;
+
+        case IO_SLOT_DORMANT:
+            /* Huh?? But we just queued a write. */
+            abort();
+
+        case IO_SLOT_COMPLETE:
+            if (sock->slot_write.succeeded)
+            {
+                /* TODO: more partial length handling */
+                return len;
+            }
+            else
+            {
+                return -1;
+            }
+
+        default:
+            abort();
+    }
+}
+
+static void
+obfs_test_win32_close(openvpn_transport_socket_t handle)
+{
+    free_socket((struct obfs_test_socket_win32 *) handle);
+}
+
+void
+obfs_test_initialize_vtabs_platform(void)
+{
+    obfs_test_bind_vtab.bind = obfs_test_win32_bind;
+    obfs_test_socket_vtab.request_event = obfs_test_win32_request_event;
+    obfs_test_socket_vtab.update_event = obfs_test_win32_update_event;
+    obfs_test_socket_vtab.pump = obfs_test_win32_pump;
+    obfs_test_socket_vtab.recvfrom = obfs_test_win32_recvfrom;
+    obfs_test_socket_vtab.sendto = obfs_test_win32_sendto;
+    obfs_test_socket_vtab.close = obfs_test_win32_close;
+}
diff --git a/src/plugins/obfs-test/obfs-test.c b/src/plugins/obfs-test/obfs-test.c
new file mode 100644
index 00000000..27a3d21e
--- /dev/null
+++ b/src/plugins/obfs-test/obfs-test.c
@@ -0,0 +1,94 @@ 
+#include <stdlib.h>
+#include <string.h>
+#include <stdbool.h>
+#include "openvpn-plugin.h"
+#include "openvpn-transport.h"
+#include "obfs-test.h"
+
+struct openvpn_transport_bind_vtab1 obfs_test_bind_vtab = { 0 };
+struct openvpn_transport_socket_vtab1 obfs_test_socket_vtab = { 0 };
+
+struct obfs_test_context
+{
+    struct openvpn_plugin_callbacks *global_vtab;
+};
+
+static void
+free_context(struct obfs_test_context *context)
+{
+    if (!context)
+    {
+        return;
+    }
+    free(context);
+}
+
+OPENVPN_EXPORT int
+openvpn_plugin_open_v3(int version, struct openvpn_plugin_args_open_in const *args,
+                       struct openvpn_plugin_args_open_return *out)
+{
+    struct obfs_test_context *context;
+
+    context = (struct obfs_test_context *) calloc(1, sizeof(struct obfs_test_context));
+    if (!context)
+    {
+        return OPENVPN_PLUGIN_FUNC_ERROR;
+    }
+
+    context->global_vtab = args->callbacks;
+    obfs_test_initialize_vtabs_platform();
+    obfs_test_bind_vtab.parseargs = obfs_test_parseargs;
+    obfs_test_bind_vtab.argerror = obfs_test_argerror;
+    obfs_test_bind_vtab.freeargs = obfs_test_freeargs;
+
+    out->type_mask = OPENVPN_PLUGIN_MASK(OPENVPN_PLUGIN_TRANSPORT);
+    out->handle = (openvpn_plugin_handle_t *) context;
+    return OPENVPN_PLUGIN_FUNC_SUCCESS;
+
+err:
+    free_context(context);
+    return OPENVPN_PLUGIN_FUNC_ERROR;
+}
+
+OPENVPN_EXPORT void
+openvpn_plugin_close_v1(openvpn_plugin_handle_t handle)
+{
+    free_context((struct obfs_test_context *) handle);
+}
+
+OPENVPN_EXPORT int
+openvpn_plugin_func_v3(int version,
+                       struct openvpn_plugin_args_func_in const *arguments,
+                       struct openvpn_plugin_args_func_return *retptr)
+{
+    /* We don't ask for any bits that use this interface. */
+    return OPENVPN_PLUGIN_FUNC_ERROR;
+}
+
+OPENVPN_EXPORT void *
+openvpn_plugin_get_vtab_v1(int selector, size_t *size_out)
+{
+    switch (selector)
+    {
+        case OPENVPN_VTAB_TRANSPORT_BIND_V1:
+            if (obfs_test_bind_vtab.bind == NULL)
+            {
+                return NULL;
+            }
+            *size_out = sizeof(struct openvpn_transport_bind_vtab1);
+            return &obfs_test_bind_vtab;
+
+        default:
+            return NULL;
+    }
+}
+
+void
+obfs_test_log(struct obfs_test_context *ctx,
+              openvpn_plugin_log_flags_t flags, const char *fmt, ...)
+{
+    va_list va;
+    va_start(va, fmt);
+    ctx->global_vtab->plugin_vlog(flags, OBFS_TEST_PLUGIN_NAME, fmt, va);
+    va_end(va);
+}
diff --git a/src/plugins/obfs-test/obfs-test.exports b/src/plugins/obfs-test/obfs-test.exports
new file mode 100644
index 00000000..e7baada4
--- /dev/null
+++ b/src/plugins/obfs-test/obfs-test.exports
@@ -0,0 +1,4 @@ 
+openvpn_plugin_open_v3
+openvpn_plugin_close_v1
+openvpn_plugin_get_vtab_v1
+openvpn_plugin_func_v3
diff --git a/src/plugins/obfs-test/obfs-test.h b/src/plugins/obfs-test/obfs-test.h
new file mode 100644
index 00000000..b9a6f8b4
--- /dev/null
+++ b/src/plugins/obfs-test/obfs-test.h
@@ -0,0 +1,42 @@ 
+#ifndef OPENVPN_PLUGIN_OBFS_TEST_H
+#define OPENVPN_PLUGIN_OBFS_TEST_H 1
+
+#include "openvpn-plugin.h"
+#include "openvpn-transport.h"
+
+#define OBFS_TEST_PLUGIN_NAME "obfs-test"
+
+struct obfs_test_context;
+
+struct obfs_test_args
+{
+    const char *error;
+    int offset;
+};
+
+extern struct openvpn_transport_bind_vtab1 obfs_test_bind_vtab;
+extern struct openvpn_transport_socket_vtab1 obfs_test_socket_vtab;
+
+void obfs_test_initialize_vtabs_platform(void);
+
+void obfs_test_munge_addr(struct sockaddr *addr, openvpn_transport_socklen_t len);
+
+size_t obfs_test_max_munged_buf_size(size_t clear_size);
+
+size_t obfs_test_munge_buf(struct obfs_test_args *how,
+                           char *out, const char *in, size_t len);
+
+ssize_t obfs_test_unmunge_buf(struct obfs_test_args *how,
+                              char *buf, size_t len);
+
+openvpn_transport_args_t obfs_test_parseargs(void *plugin_handle,
+                                             const char *const *argv, int argc);
+
+const char *obfs_test_argerror(openvpn_transport_args_t args);
+
+void obfs_test_freeargs(openvpn_transport_args_t args);
+
+void obfs_test_log(struct obfs_test_context *ctx,
+                   openvpn_plugin_log_flags_t flags, const char *fmt, ...);
+
+#endif /* !OPENVPN_PLUGIN_OBFS_TEST_H */