/* $Id: obsdrdr.c.html,v 1.1 2006/05/31 21:21:41 nanard Exp $ */
/* miniupnp project : http://miniupnp.free.fr/
 *
 * Copyright (c) 2006 Thomas Bernard
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions
 * are met:
 *
 *    - Redistributions of source code must retain the above copyright
 *      notice, this list of conditions and the following disclaimer.
 *    - Redistributions in binary form must reproduce the above
 *      copyright notice, this list of conditions and the following
 *      disclaimer in the documentation and/or other materials provided
 *      with the distribution.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS
 * FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
 * COPYRIGHT HOLDERS OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
 * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES;
 * LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT
 * LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN
 * ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
 * POSSIBILITY OF SUCH DAMAGE.
 *
 */
#include <sys/types.h>
#include <sys/socket.h>
#include <net/if.h>
#include <netinet/in.h>
#include <arpa/inet.h>
#include <net/pfvar.h>
#include <fcntl.h>
#include <sys/ioctl.h>
#include <unistd.h>
#include <string.h>

#include <syslog.h>
#include <stdio.h>
#include <stdlib.h>

#include "obsdrdr.h"

static const char anchor_name[] = "miniupnpd";

void
add_redirect_rule(const char * ifname, unsigned short eport,
                  const char * iaddr, unsigned short iport)
{
    int dev;
    struct pfioc_trans io;
    struct pfioc_trans_e ioe;
    struct pfioc_pooladdr pp;
    struct pfioc_rule pr;
    struct pf_pooladdr *a;
    dev = open("/dev/pf", O_RDWR);
    if(dev<0)
    {
        syslog(LOG_ERR, "open(\"/dev/pf\"): %m");
    }
    else
    {
/* 
 * DIOCXBEGIN
 * DIOCBEGINADDRS
 * DIOCADDADDR ???
 * DIOCADDRULE
 * DIOCXCOMMIT
rdr on $ext_if proto tcp from any to any port 2022 -> 192.168.0.55 port 22
rdr on ep0 inet proto tcp from any to any port = 2022 -> 192.168.0.55 port 22
*/
        memset(&ioe, 0, sizeof(ioe));
        io.size = 1;
        io.esize = sizeof(ioe);
        io.array = &ioe;
        ioe.rs_num = PF_RULESET_RDR;
        strlcpy(ioe.anchor, anchor_name, MAXPATHLEN);
        if(ioctl(dev, DIOCXBEGIN, &io) < 0)
            syslog(LOG_ERR, "ioctl(dev, DIOCXBEGIN, ...): %m");

        memset(&pp, 0, sizeof(pp));
        strlcpy(pp.anchor, anchor_name, MAXPATHLEN);
        if(ioctl(dev, DIOCBEGINADDRS, &pp) < 0)
            syslog(LOG_ERR, "ioctl(dev, DIOCBEGINADDRS, ...): %m");

        memset(&pr, 0, sizeof(pr));
        pr.ticket = ioe.ticket;
        pr.pool_ticket = pp.ticket;

        pr.rule.dst.port_op = PF_OP_EQ;
        pr.rule.dst.port[0] = htons(eport);
        pr.rule.dst.port[1] = htons(eport);
        pr.rule.action = PF_RDR;
        pr.rule.af = AF_INET;
        strlcpy(pr.rule.ifname, ifname, IFNAMSIZ);
        pr.rule.proto = IPPROTO_TCP;
        pr.rule.rpool.proxy_port[0] = iport;
        pr.rule.rpool.proxy_port[1] = iport;
        TAILQ_INIT(&pr.rule.rpool.list);
        a = calloc(1, sizeof(struct pf_pooladdr));
        inet_pton(AF_INET, iaddr, &a->addr.v.a.addr.v4.s_addr);
        a->addr.v.a.mask.v4.s_addr = htonl(INADDR_NONE);
        TAILQ_INSERT_TAIL(&pr.rule.rpool.list, a, entries);

        strlcpy(pp.anchor, anchor_name, MAXPATHLEN);
        memcpy(&pp.addr, a, sizeof(struct pf_pooladdr));
        if(ioctl(dev, DIOCADDADDR, &pp) < 0)
            syslog(LOG_ERR, "ioctl(dev, DIOCADDADDR, ...): %m");

        strlcpy(pr.anchor, anchor_name, MAXPATHLEN);
        if(ioctl(dev, DIOCADDRULE, &pr) < 0)
            syslog(LOG_ERR, "ioctl(dev, DIOCADDRULE, ...): %m");
        else
        {
            if(ioctl(dev, DIOCXCOMMIT, &io) < 0)
                syslog(LOG_ERR, "ioctl(dev, DIOCXCOMMIT, ...): %m");
        }
        free(a);
        close(dev);
    }
}

int
add_redirect_rule2(const char * ifname, unsigned short eport,
                   const char * iaddr, unsigned short iport, int proto)
{
    int dev;
    int r;
    struct pfioc_rule pcr;
    struct pfioc_pooladdr pp;
    struct pf_pooladdr *a;
    dev = open("/dev/pf", O_RDWR);
    if(dev<0)
    {
        syslog(LOG_ERR, "open(\"/dev/pf\"): %m");
        return -1;
    }
    r = 0;
    memset(&pcr, 0, sizeof(pcr));
    strlcpy(pcr.anchor, anchor_name, MAXPATHLEN);

    memset(&pp, 0, sizeof(pp));
    strlcpy(pp.anchor, anchor_name, MAXPATHLEN);
    if(ioctl(dev, DIOCBEGINADDRS, &pp) < 0)
    {
        syslog(LOG_ERR, "ioctl(dev, DIOCBEGINADDRS, ...): %m");
        r = -1;
    }
    else
    {
        pcr.pool_ticket = pp.ticket;

        pcr.rule.dst.port_op = PF_OP_EQ;
        pcr.rule.dst.port[0] = htons(eport);
        pcr.rule.dst.port[1] = htons(eport);
        pcr.rule.action = PF_RDR;
        pcr.rule.af = AF_INET;
        strlcpy(pcr.rule.ifname, ifname, IFNAMSIZ);
        pcr.rule.proto = proto;
        pcr.rule.rpool.proxy_port[0] = iport;
        pcr.rule.rpool.proxy_port[1] = iport;
        TAILQ_INIT(&pcr.rule.rpool.list);
        a = calloc(1, sizeof(struct pf_pooladdr));
        inet_pton(AF_INET, iaddr, &a->addr.v.a.addr.v4.s_addr);
        a->addr.v.a.mask.v4.s_addr = htonl(INADDR_NONE);
        TAILQ_INSERT_TAIL(&pcr.rule.rpool.list, a, entries);

        memcpy(&pp.addr, a, sizeof(struct pf_pooladdr));
        if(ioctl(dev, DIOCADDADDR, &pp) < 0)
        {
            syslog(LOG_ERR, "ioctl(dev, DIOCADDADDR, ...): %m");
            r = -1;
        }
        else
        {
            pcr.action = PF_CHANGE_GET_TICKET;
            if(ioctl(dev, DIOCCHANGERULE, &pcr) < 0)
            {
                syslog(LOG_ERR, "ioctl(dev, DIOCCHANGERULE, ...) PF_CHANGE_GET_TICKET: %m");
                r = -1;
            }
            else
            {
                pcr.action = PF_CHANGE_ADD_TAIL;
                if(ioctl(dev, DIOCCHANGERULE, &pcr) < 0)
                {
                    syslog(LOG_ERR, "ioctl(dev, DIOCCHANGERULE, ...) PF_CHANGE_ADD_TAIL: %m");
                    r = -1;
                }
            }
        }
        free(a);
    }
    close(dev);
    return r;
}

/* get_redirect_rule()
 * return value : 0 success (found)
 * -1 = error */
int
get_redirect_rule(const char * ifname, unsigned short eport, int proto,
                  char * iaddr, int iaddrlen, unsigned short * iport)
{
    int dev;
    int i, n;
    struct pfioc_rule pr;
    struct pfioc_pooladdr pp;
    dev = open("/dev/pf", O_RDWR);
    if(dev<0)
    {
        syslog(LOG_ERR, "open(\"/dev/pf\"): %m");
        return -1;
    }
    memset(&pr, 0, sizeof(pr));
    strlcpy(pr.anchor, anchor_name, MAXPATHLEN);
    pr.rule.action = PF_RDR;
    if(ioctl(dev, DIOCGETRULES, &pr) < 0)
    {
        syslog(LOG_ERR, "ioctl(dev, DIOCGETRULES, ...): %m");
        goto error;
    }
    n = pr.nr;
    for(i=0; i<n; i++)
    {
        pr.nr = i;
        if(ioctl(dev, DIOCGETRULE, &pr) < 0)
        {
            syslog(LOG_ERR, "ioctl(dev, DIOCGETRULE): %m");
            goto error;
        }
        if( (eport == ntohs(pr.rule.dst.port[0]))
          && (eport == ntohs(pr.rule.dst.port[1]))
          && (pr.rule.proto == proto) )
        {
            *iport = pr.rule.rpool.proxy_port[0];
            memset(&pp, 0, sizeof(pp));
            strlcpy(pp.anchor, anchor_name, MAXPATHLEN);
            pp.r_action = PF_RDR;
            pp.r_num = i;
            pp.ticket = pr.ticket;
            if(ioctl(dev, DIOCGETADDRS, &pp) < 0)
            {
                syslog(LOG_ERR, "ioctl(dev, DIOCGETADDRS, ...): %m");
                goto error;
            }
            if(pp.nr != 1)
            {
                syslog(LOG_NOTICE, "no address associated with pf rule!");
                goto error;
            }
            pp.nr = 0;  // 1er
            if(ioctl(dev, DIOCGETADDR, &pp) < 0)
            {
                syslog(LOG_ERR, "ioctl(dev, DIOCGETADDR, ...): %m");
                goto error;
            }
            inet_ntop(AF_INET, &pp.addr.addr.v.a.addr.v4.s_addr,
                      iaddr, iaddrlen);
            close(dev);
            return 0;
        }
    }
error:
    close(dev);
    return -1;
}

int
delete_redirect_rule(const char * ifname, unsigned short eport, int proto)
{
    int dev;
    int i, n;
    struct pfioc_rule pr;
    dev = open("/dev/pf", O_RDWR);
    if(dev<0)
    {
        syslog(LOG_ERR, "open(\"/dev/pf\"): %m");
        return -1;
    }
    memset(&pr, 0, sizeof(pr));
    strlcpy(pr.anchor, anchor_name, MAXPATHLEN);
    pr.rule.action = PF_RDR;
    if(ioctl(dev, DIOCGETRULES, &pr) < 0)
    {
        syslog(LOG_ERR, "ioctl(dev, DIOCGETRULES, ...): %m");
        goto error;
    }
    n = pr.nr;
    for(i=0; i<n; i++)
    {
        pr.nr = i;
        if(ioctl(dev, DIOCGETRULE, &pr) < 0)
        {
            syslog(LOG_ERR, "ioctl(dev, DIOCGETRULE): %m");
            goto error;
        }
        if( (eport == ntohs(pr.rule.dst.port[0]))
          && (eport == ntohs(pr.rule.dst.port[1]))
          && (pr.rule.proto == proto) )
        {
            pr.action = PF_CHANGE_GET_TICKET;
            if(ioctl(dev, DIOCCHANGERULE, &pr) < 0)
            {
                syslog(LOG_ERR, "ioctl(dev, DIOCCHANGERULE, ...) PF_CHANGE_GET_TICKET: %m");
                goto error;
            }
            pr.action = PF_CHANGE_REMOVE;
            pr.nr = i;
            if(ioctl(dev, DIOCCHANGERULE, &pr) < 0)
            {
                syslog(LOG_ERR, "ioctl(dev, DIOCCHANGERULE, ...) PF_CHANGE_REMOVE: %m");
                goto error;
            }
            close(dev);
            return 0;
        }
    }
error:
    close(dev);
    return -1;
}

/* this function is only for testing */
void
list_rules(void)
{
    char buf[32];
    int dev;
    int i, n;
    struct pfioc_rule pr;
    struct pfioc_pooladdr pp;
    dev = open("/dev/pf", O_RDWR);
    if(dev<0)
    {
        perror("open(\"/dev/pf\")");
        syslog(LOG_ERR, "open(\"/dev/pf\"): %m");
        return ;//-1;
    }
    memset(&pr, 0, sizeof(pr));
    strlcpy(pr.anchor, anchor_name, MAXPATHLEN);
    pr.rule.action = PF_RDR;
    if(ioctl(dev, DIOCGETRULES, &pr) < 0)
        perror("DIOCGETRULES");
    printf("ticket = %d, nr = %d\n", pr.ticket, pr.nr);
    n = pr.nr;
    for(i=0; i<n; i++)
    {
        printf("-- rule %d --\n", i);
        pr.nr = i;
        if(ioctl(dev, DIOCGETRULE, &pr) < 0)
            perror("DIOCGETRULE");
        printf(" %d:%d -> %d:%d  proto %d\n",
            (int)ntohs(pr.rule.dst.port[0]),
            (int)ntohs(pr.rule.dst.port[1]),
            (int)pr.rule.rpool.proxy_port[0],
            (int)pr.rule.rpool.proxy_port[1],
            (int)pr.rule.proto);
        memset(&pp, 0, sizeof(pp));
        strlcpy(pp.anchor, anchor_name, MAXPATHLEN);
        pp.r_action = PF_RDR;
        pp.r_num = i;
        pp.ticket = pr.ticket;
        if(ioctl(dev, DIOCGETADDRS, &pp) < 0)
            perror("DIOCGETADDRS");
        printf("  nb pool addr = %d ticket=%d\n", pp.nr, pp.ticket);
        //if(ioctl(dev, DIOCGETRULE, &pr) < 0)
        //  perror("DIOCGETRULE");
        pp.nr = 0;  // 1er
        if(ioctl(dev, DIOCGETADDR, &pp) < 0)
            perror("DIOCGETADDR");
        // addr.v.a.addr.v4.s_addr
        printf("  %s\n", inet_ntop(AF_INET, &pp.addr.addr.v.a.addr.v4.s_addr, buf, 32));
    }
    close(dev);
}