root/usr/src/lib/libnsl/rpc/ti_opts.c
/*
 * CDDL HEADER START
 *
 * The contents of this file are subject to the terms of the
 * Common Development and Distribution License (the "License").
 * You may not use this file except in compliance with the License.
 *
 * You can obtain a copy of the license at usr/src/OPENSOLARIS.LICENSE
 * or http://www.opensolaris.org/os/licensing.
 * See the License for the specific language governing permissions
 * and limitations under the License.
 *
 * When distributing Covered Code, include this CDDL HEADER in each
 * file and include the License file at usr/src/OPENSOLARIS.LICENSE.
 * If applicable, add the following below this CDDL HEADER, with the
 * fields enclosed by brackets "[]" replaced with your own identifying
 * information: Portions Copyright [yyyy] [name of copyright owner]
 *
 * CDDL HEADER END
 */

/*
 * Copyright 2006 Sun Microsystems, Inc.  All rights reserved.
 * Use is subject to license terms.
 */

/* Copyright (c) 1983, 1984, 1985, 1986, 1987, 1988, 1989 AT&T */
/* All Rights Reserved */
/*
 * Portions of this source code were derived from Berkeley
 * 4.3 BSD under license from the Regents of the University of
 * California.
 */

#include "mt.h"
#include <stdio.h>
#include <netinet/in.h>
#include <netinet/tcp.h>
#include <netinet/udp.h>
#include <inttypes.h>
#include <sys/types.h>
#include <tiuser.h>
#include <sys/socket.h>
#include <net/if.h>
#include <sys/sockio.h>
#include <rpc/rpc.h>
#include <sys/tl.h>
#include <sys/stropts.h>
#include <errno.h>
#include <libintl.h>
#include <string.h>
#include <strings.h>
#include <syslog.h>
#include <unistd.h>
#include <ucred.h>
#include <alloca.h>
#include <stdlib.h>
#include <zone.h>
#include <tsol/label.h>

extern bool_t __svc_get_door_ucred(const SVCXPRT *, ucred_t *);

/*
 * This routine is typically called on the server side if the server
 * wants to know the caller ucred.  Called typically by rpcbind.  It
 * depends upon the t_optmgmt call to local transport driver so that
 * return the uid value in options in T_CONN_IND, T_CONN_CON and
 * T_UNITDATA_IND.
 * With the advent of the credential in the mblk, this is simply
 * extended to all transports when the packet travels over the
 * loopback network; for UDP we use a special socket option and for
 * tcp we don't need to do any setup, we just call getpeerucred()
 * later.
 */

/*
 * Version for Solaris with new local transport code and ucred.
 */
int
__rpc_negotiate_uid(int fd)
{
        struct strioctl strioc;
        unsigned int set = 1;

        /* For tcp we use getpeerucred and it needs no initialization. */
        if (ioctl(fd, I_FIND, "tcp") > 0)
                return (0);

        strioc.ic_cmd = TL_IOC_UCREDOPT;
        strioc.ic_timout = -1;
        strioc.ic_len = (int)sizeof (unsigned int);
        strioc.ic_dp = (char *)&set;

        if (ioctl(fd, I_STR, &strioc) == -1 &&
            __rpc_tli_set_options(fd, SOL_SOCKET, SO_RECVUCRED, 1) == -1) {
                syslog(LOG_ERR, "rpc_negotiate_uid (%s): %m",
                    "ioctl:I_STR:TL_IOC_UCREDOPT/SO_RECVUCRED");
                return (-1);
        }
        return (0);
}

void
svc_fd_negotiate_ucred(int fd)
{
        (void) __rpc_negotiate_uid(fd);
}


/*
 * This returns the ucred of the caller.  It assumes that the optbuf
 * information is stored at xprt->xp_p2.
 * There are three distinct cases: the option buffer is headed
 * with a "struct opthdr" and the credential option is the only
 * one, or it's a T_opthdr and our option may follow others; or there
 * are no options and we attempt getpeerucred().
 */
static int
find_ucred_opt(const SVCXPRT *trans, ucred_t *uc, bool_t checkzone)
{
        /* LINTED pointer alignment */
        struct netbuf *abuf = (struct netbuf *)trans->xp_p2;
        char *bufp, *maxbufp;
        struct opthdr *opth;
        static zoneid_t myzone = MIN_ZONEID - 1;        /* invalid */

        if (abuf == NULL || abuf->buf == NULL) {
                if (getpeerucred(trans->xp_fd, &uc) == 0)
                        goto verifyzone;
                return (-1);
        }

#ifdef RPC_DEBUG
        syslog(LOG_INFO, "find_ucred_opt %p %x", abuf->buf, abuf->len);
#endif
        /* LINTED pointer cast */
        opth = (struct opthdr *)abuf->buf;
        if (opth->level == TL_PROT_LEVEL &&
            opth->name == TL_OPT_PEER_UCRED &&
            opth->len + sizeof (*opth) == abuf->len) {
#ifdef RPC_DEBUG
                syslog(LOG_INFO, "find_ucred_opt (opthdr): OK!");
#endif
                (void) memcpy(uc, &opth[1], opth->len);
                /*
                 * Always from inside our zone because zones use a separate name
                 * space for loopback; at this time, the kernel may send a
                 * packet pretending to be from the global zone when it's
                 * really from our zone so we skip the zone check.
                 */
                return (0);
        }

        bufp = abuf->buf;
        maxbufp = bufp + abuf->len;

        while (bufp + sizeof (struct T_opthdr) < maxbufp) {
                /* LINTED pointer cast */
                struct T_opthdr *opt = (struct T_opthdr *)bufp;

#ifdef RPC_DEBUG
                syslog(LOG_INFO, "find_ucred_opt opt: %p %x, %d %d", opt,
                        opt->len, opt->name, opt->level);
#endif
                if (opt->len > maxbufp - bufp || (opt->len & 3))
                        return (-1);
                if (opt->level == SOL_SOCKET && opt->name == SCM_UCRED &&
                    opt->len - sizeof (struct T_opthdr) <= ucred_size()) {
#ifdef RPC_DEBUG
                        syslog(LOG_INFO, "find_ucred_opt (T_opthdr): OK!");
#endif
                        (void) memcpy(uc, &opt[1],
                            opt->len - sizeof (struct T_opthdr));
                        goto verifyzone;
                }
                bufp += opt->len;
        }
        if (getpeerucred(trans->xp_fd, &uc) != 0)
                return (-1);
verifyzone:
        if (!checkzone)
                return (0);

        if (myzone == MIN_ZONEID - 1)
                myzone = getzoneid();

        /* Return 0 only for the local zone */
        return (ucred_getzoneid(uc) == myzone ? 0 : -1);
}

/*
 * Version for Solaris with new local transport code
 */
int
__rpc_get_local_uid(SVCXPRT *trans, uid_t *uid_out)
{
        ucred_t *uc = alloca(ucred_size());
        int err;

        /* LINTED - pointer alignment */
        if (svc_type(trans) == SVC_DOOR)
                err = __svc_get_door_ucred(trans, uc) == FALSE;
        else
                err = find_ucred_opt(trans, uc, B_TRUE);

        if (err != 0)
                return (-1);
        *uid_out = ucred_geteuid(uc);
        return (0);
}

/*
 * Return local credentials.
 */
bool_t
__rpc_get_local_cred(SVCXPRT *xprt, svc_local_cred_t *lcred)
{
        ucred_t *uc = alloca(ucred_size());
        int err;

        /* LINTED - pointer alignment */
        if (svc_type(xprt) == SVC_DOOR)
                err = __svc_get_door_ucred(xprt, uc) == FALSE;
        else
                err = find_ucred_opt(xprt, uc, B_TRUE);

        if (err != 0)
                return (FALSE);

        lcred->euid = ucred_geteuid(uc);
        lcred->egid = ucred_getegid(uc);
        lcred->ruid = ucred_getruid(uc);
        lcred->rgid = ucred_getrgid(uc);
        lcred->pid = ucred_getpid(uc);
        return (TRUE);
}

/*
 * Return local ucred.
 */
int
svc_getcallerucred(const SVCXPRT *trans, ucred_t **uc)
{
        ucred_t *ucp = *uc;
        int err;

        if (ucp == NULL) {
                ucp = malloc(ucred_size());
                if (ucp == NULL)
                        return (-1);
        }

        /* LINTED - pointer alignment */
        if (svc_type(trans) == SVC_DOOR)
                err = __svc_get_door_ucred(trans, ucp) == FALSE;
        else
                err = find_ucred_opt(trans, ucp, B_FALSE);

        if (err != 0) {
                if (*uc == NULL)
                        free(ucp);
                return (-1);
        }

        if (*uc == NULL)
                *uc = ucp;

        return (0);
}


/*
 * get local ip address
 */
int
__rpc_get_ltaddr(struct netbuf *nbufp, struct netbuf *ltaddr)
{
        unsigned int total_optlen;
        struct T_opthdr *opt, *opt_start = NULL, *opt_end;
        struct sockaddr_in *ipv4sa;
        struct sockaddr_in6 *ipv6sa;
        int s;
        struct sioc_addrreq areq;

        if (nbufp == (struct netbuf *)0 || ltaddr == (struct netbuf *)0) {
                t_errno = TBADOPT;
                return (-1);
        }

        total_optlen = nbufp->len;
        if (total_optlen == 0)
                return (1);

        /* LINTED pointer alignment */
        opt_start = (struct T_opthdr *)nbufp->buf;
        if (opt_start == NULL) {
                t_errno = TBADOPT;
                return (-1);
        }

        /* Make sure the start of the buffer is aligned */
        if (!(__TPI_TOPT_ISALIGNED(opt_start))) {
                t_errno = TBADOPT;
                return (-1);
        }

        /* LINTED pointer alignment */
        opt_end = (struct T_opthdr *)((uchar_t *)opt_start + total_optlen);
        opt = opt_start;

        /*
         * Look for the desired option header
         */
        do {
                if (((uchar_t *)opt + sizeof (struct T_opthdr)) >
                    (uchar_t *)opt_end) {
                        t_errno = TBADOPT;
                        return (-1);
                }
                if (opt->len < sizeof (struct T_opthdr)) {
                        t_errno = TBADOPT;
                        return (-1);
                }
                if (((uchar_t *)opt + opt->len) > (uchar_t *)opt_end) {
                        t_errno = TBADOPT;
                        return (-1);
                }
                switch (opt->level) {
                case IPPROTO_IP:
                        if (opt->name == IP_RECVDSTADDR) {
                                struct sockaddr_in v4tmp;

                                opt++;
                                if (((uchar_t *)opt + sizeof (struct in_addr)) >
                                    (uchar_t *)opt_end) {
                                        t_errno = TBADOPT;
                                        return (-1);
                                }
                                bzero(&v4tmp, sizeof (v4tmp));
                                v4tmp.sin_family = AF_INET;
                                v4tmp.sin_addr = *(struct in_addr *)opt;
#ifdef RPC_DEBUG
                                {
                                struct in_addr ia;
                                char str[INET_ADDRSTRLEN];

                                ia = *(struct in_addr *)opt;
                                (void) inet_ntop(AF_INET, &ia,
                                                str, sizeof (str));
                                syslog(LOG_INFO,
                                    "__rpc_get_ltaddr for IP_RECVDSTADDR: %s",
                                        str);
                                }
#endif
                                if ((s = open("/dev/udp", O_RDONLY)) < 0) {
#ifdef RPC_DEBUG
                                        syslog(LOG_ERR, "__rpc_get_ltaddr: "
                                            "dev udp open failed");
#endif
                                        return (1);
                                }

                                (void) memcpy(&areq.sa_addr, &v4tmp,
                                                                sizeof (v4tmp));
                                areq.sa_res = -1;
                                if (ioctl(s, SIOCTMYADDR, (caddr_t)&areq) < 0) {
                                        syslog(LOG_ERR,
                                            "get_ltaddr:ioctl for udp failed");
                                        (void) close(s);
                                        return (1);
                                }
                                (void) close(s);
                                if (areq.sa_res == 1) {
                                    /* LINTED pointer cast */
                                    ipv4sa = (struct sockaddr_in *)ltaddr->buf;
                                    ipv4sa->sin_family = AF_INET;
                                    ipv4sa->sin_addr = *(struct in_addr *)opt;
                                    return (0);
                                } else
                                    return (1);

                        }
                        break;
                case IPPROTO_IPV6:
                        if (opt->name == IPV6_PKTINFO) {
                                struct sockaddr_in6 v6tmp;
                                opt++;
                                if (((uchar_t *)opt +
                                    sizeof (struct in6_pktinfo)) >
                                    (uchar_t *)opt_end) {
                                        t_errno = TBADOPT;
                                        return (-1);
                                }
                                bzero(&v6tmp, sizeof (v6tmp));
                                v6tmp.sin6_family = AF_INET6;
                                v6tmp.sin6_addr =
                                        ((struct in6_pktinfo *)opt)->ipi6_addr;
#ifdef RPC_DEBUG
                                {
                                struct in6_pktinfo *in6_pkt;
                                char str[INET6_ADDRSTRLEN];

                                in6_pkt = (struct in6_pktinfo *)opt;
                                (void) inet_ntop(AF_INET6, &in6_pkt->ipi6_addr,
                                                str, sizeof (str));
                                syslog(LOG_INFO,
                                        "__rpc_get_ltaddr for IPV6_PKTINFO: %s",
                                        str);
                                }
#endif
                                if ((s = open("/dev/udp6", O_RDONLY)) < 0) {
#ifdef RPC_DEBUG
                                        syslog(LOG_ERR, "__rpc_get_ltaddr: "
                                            "dev udp6 open failed");
#endif
                                        return (1);
                                }

                                (void) memcpy(&areq.sa_addr, &v6tmp,
                                                                sizeof (v6tmp));
                                areq.sa_res = -1;
                                if (ioctl(s, SIOCTMYADDR, (caddr_t)&areq) < 0) {
                                        syslog(LOG_ERR,
                                            "get_ltaddr:ioctl for udp6 failed");
                                        (void) close(s);
                                        return (1);
                                }
                                (void) close(s);
                                if (areq.sa_res == 1) {
                                    /* LINTED pointer cast */
                                    ipv6sa = (struct sockaddr_in6 *)ltaddr->buf;
                                    ipv6sa->sin6_family = AF_INET6;
                                    ipv6sa->sin6_addr =
                                        ((struct in6_pktinfo *)opt)->ipi6_addr;

                                    return (0);
                                } else
                                    return (1);
                        }
                        break;
                default:
                        break;
                }
                /* LINTED improper alignment */
                opt = (struct T_opthdr *)((uchar_t *)opt +
                            __TPI_ALIGN(opt->len));
        } while (opt < opt_end);
        return (1);
}

#define __TRANSPORT_INDSZ       128

int
__rpc_tli_set_options(int fd, int optlevel, int optname, int optval)
{
        struct t_optmgmt oreq, ores;
        struct opthdr *topt;
        int *ip;
        int optsz;
        char buf[__TRANSPORT_INDSZ];


        switch (optname) {
        case SO_DONTLINGER: {
                struct linger *ling;
                /* LINTED */
                ling = (struct linger *)
                        (buf + sizeof (struct opthdr));
                ling->l_onoff = 0;
                optsz = sizeof (struct linger);
                break;
        }

        case SO_LINGER: {
                struct linger *ling;
                /* LINTED */
                ling = (struct linger *)
                        (buf + sizeof (struct opthdr));
                ling->l_onoff = 1;
                ling->l_linger = (int)optval;
                optsz = sizeof (struct linger);
                break;
        }
        case IP_RECVDSTADDR:
        case IPV6_RECVPKTINFO:
        case SO_DEBUG:
        case SO_KEEPALIVE:
        case SO_DONTROUTE:
        case SO_USELOOPBACK:
        case SO_REUSEADDR:
        case SO_DGRAM_ERRIND:
        case SO_RECVUCRED:
        case SO_ANON_MLP:
        case SO_MAC_EXEMPT:
        case SO_EXCLBIND:
        case TCP_EXCLBIND:
        case UDP_EXCLBIND:
                /* LINTED */
                ip = (int *)(buf + sizeof (struct opthdr));
                *ip = optval;
                optsz = sizeof (int);
                break;
        default:
                return (-1);
        }

        /* LINTED */
        topt = (struct opthdr *)buf;
        topt->level =  optlevel;
        topt->name = optname;
        topt->len = optsz;
        oreq.flags = T_NEGOTIATE;
        oreq.opt.len = sizeof (struct opthdr) + optsz;
        oreq.opt.buf = buf;

        ores.flags = 0;
        ores.opt.buf = buf;
        ores.opt.maxlen = __TRANSPORT_INDSZ;
        if (t_optmgmt(fd, &oreq, &ores) < 0 ||
            ores.flags != T_SUCCESS) {
                return (-1);
        }
        return (0);
}

/*
 * Format an error message corresponding to the given TLI and system error
 * codes.
 */

void
__tli_sys_strerror(char *buf, size_t buflen, int tli_err, int sys_err)
{
        char *errorstr;

        if (tli_err == TSYSERR) {
                errorstr = strerror(sys_err);
                if (errorstr == NULL)
                        (void) snprintf(buf, buflen,
                                        dgettext(__nsl_dom,
                                                "Unknown system error %d"),
                                        sys_err);
                else
                        (void) strlcpy(buf, errorstr, buflen);
        } else {
                errorstr = t_strerror(tli_err);
                (void) strlcpy(buf, errorstr, buflen);
        }
}

/*
 * Depending on the specified RPC number, attempt to set mac_exempt
 * option on the opened socket; these requests need to be able to do MAC
 * MAC read-down operations.  Privilege is needed to set this option.
 */

void
__rpc_set_mac_options(int fd, const struct netconfig *nconf, rpcprog_t prognum)
{
        int ret = 0;

        if (!is_system_labeled())
                return;

        if (strcmp(nconf->nc_protofmly, NC_INET) != 0 &&
            strcmp(nconf->nc_protofmly, NC_INET6) != 0)
                return;

        if (is_multilevel(prognum)) {
                ret = __rpc_tli_set_options(fd, SOL_SOCKET, SO_MAC_EXEMPT, 1);
                if (ret < 0) {
                        char errorstr[100];

                        __tli_sys_strerror(errorstr, sizeof (errorstr),
                            t_errno, errno);
                        (void) syslog(LOG_ERR, "rpc_set_mac_options: %s",
                            errorstr);
                }
        }
}