Plan 9 from Bell Labs’s /usr/web/sources/plan9/sys/src/cmd/auth/factotum/util.c

Copyright © 2021 Plan 9 Foundation.
Distributed under the MIT License.
Download the Plan 9 distribution.


#include "dat.h"

static char secstore[100];   /* server name */

/* bind in the default network and cs */
static int
bindnetcs(void)
{
	int srvfd;

	if(access("/net/tcp", AEXIST) < 0)
		bind("#I", "/net", MBEFORE);

	if(access("/net/cs", AEXIST) < 0){
		if((srvfd = open("#s/cs", ORDWR)) >= 0){
			if(mount(srvfd, -1, "/net", MBEFORE, "") >= 0)
				return 0;
			close(srvfd);
		}
		return -1;
	}
	return 0;
}

int
_authdial(char *net, char *authdom)
{
	int fd, vanilla;

	vanilla = net==nil || strcmp(net, "/net")==0;

	if(!vanilla || bindnetcs()>=0)
		return authdial(net, authdom);

	/*
	 * If we failed to mount /srv/cs, assume that
	 * we're still bootstrapping the system and dial
	 * the one auth server passed to us on the command line.
	 * In normal operation, it is important *not* to do this,
	 * because the bootstrap auth server is only good for
	 * a single auth domain.
	 *
	 * The ticket request code should really check the
	 * remote authentication domain too.
	 */

	/* use the auth server passed to us as an arg */
	if(authaddr == nil)
		return -1;
	fd = dial(netmkaddr(authaddr, "tcp", "567"), 0, 0, 0);
	if(fd >= 0)
		return fd;
	return dial(netmkaddr(authaddr, "il", "566"), 0, 0, 0);
}

int
secdial(void)
{
	char *p, buf[80], *f[3];
	int fd, nf;

	p = secstore; /* take it from writehostowner, if set there */
	if(*p == 0)	  /* else use the authserver */
		p = "$auth";

	if(bindnetcs() >= 0)
		return dial(netmkaddr(p, "net", "secstore"), 0, 0, 0);

	/* translate $auth ourselves.
	 * authaddr is something like il!host!566 or tcp!host!567.
	 * extract host, accounting for a change of format to something
	 * like il!host or tcp!host or host.
	 */
	if(strcmp(p, "$auth")==0){
		if(authaddr == nil)
			return -1;
		safecpy(buf, authaddr, sizeof buf);
		nf = getfields(buf, f, nelem(f), 0, "!");
		switch(nf){
		default:
			return -1;
		case 1:
			p = f[0];
			break;
		case 2:
		case 3:
			p = f[1];
			break;
		}
	}
	fd = dial(netmkaddr(p, "tcp", "5356"), 0, 0, 0);
	if(fd >= 0)
		return fd;
	return -1;
}
/*
 *  prompt user for a key.  don't care about memory leaks, runs standalone
 */
static Attr*
promptforkey(char *params)
{
	char *v;
	int fd;
	Attr *a, *attr;
	char *def;

	fd = open("/dev/cons", ORDWR);
	if(fd < 0)
		sysfatal("opening /dev/cons: %r");

	attr = _parseattr(params);
	fprint(fd, "\n!Adding key:");
	for(a=attr; a; a=a->next)
		if(a->type != AttrQuery && a->name[0] != '!')
			fprint(fd, " %q=%q", a->name, a->val);
	fprint(fd, "\n");

	for(a=attr; a; a=a->next){
		v = a->name;
		if(a->type != AttrQuery || v[0]=='!')
			continue;
		def = nil;
		if(strcmp(v, "user") == 0)
			def = getuser();
		a->val = readcons(v, def, 0);
		if(a->val == nil)
			sysfatal("user terminated key input");
		a->type = AttrNameval;
	}
	for(a=attr; a; a=a->next){
		v = a->name;
		if(a->type != AttrQuery || v[0]!='!')
			continue;
		def = nil;
		if(strcmp(v+1, "user") == 0)
			def = getuser();
		a->val = readcons(v+1, def, 1);
		if(a->val == nil)
			sysfatal("user terminated key input");
		a->type = AttrNameval;
	}
	fprint(fd, "!\n");
	close(fd);
	return attr;
}

/*
 *  send a key to the mounted factotum
 */
static int
sendkey(Attr *attr)
{
	int fd, rv;
	char buf[1024];

	fd = open("/mnt/factotum/ctl", ORDWR);
	if(fd < 0)
		sysfatal("opening /mnt/factotum/ctl: %r");
	rv = fprint(fd, "key %A\n", attr);
	read(fd, buf, sizeof buf);
	close(fd);
	return rv;
}

/* askuser */
void
askuser(char *params)
{
	Attr *attr;

	attr = promptforkey(params);
	if(attr == nil)
		sysfatal("no key supplied");
	if(sendkey(attr) < 0)
		sysfatal("sending key to factotum: %r");
}

ulong conftaggen;
int
canusekey(Fsstate *fss, Key *k)
{
	int i;

	if(_strfindattr(k->attr, "confirm")){
		for(i=0; i<fss->nconf; i++)
			if(fss->conf[i].key == k)
				return fss->conf[i].canuse;
		if(fss->nconf%16 == 0)
			fss->conf = erealloc(fss->conf, (fss->nconf+16)*(sizeof(fss->conf[0])));
		fss->conf[fss->nconf].key = k;
		k->ref++;
		fss->conf[fss->nconf].canuse = -1;
		fss->conf[fss->nconf].tag = conftaggen++;
		fss->nconf++;
		return -1;
	}
	return 1;
}

/* closekey */
void
closekey(Key *k)
{
	if(k == nil)
		return;
	if(--k->ref != 0)
		return;
	if(k->proto && k->proto->closekey)
		(*k->proto->closekey)(k);
	_freeattr(k->attr);
	_freeattr(k->privattr);
	k->attr = (void*)~1;
	k->privattr = (void*)~1;
	k->proto = nil;
	free(k);
}

static uchar*
pstring(uchar *p, uchar *e, char *s)
{
	uint n;

	if(p == nil)
		return nil;
	if(s == nil)
		s = "";
	n = strlen(s);
	if(p+n+BIT16SZ >= e)
		return nil;
	PBIT16(p, n);
	p += BIT16SZ;
	memmove(p, s, n);
	p += n;
	return p;
}

static uchar*
pcarray(uchar *p, uchar *e, uchar *s, uint n)
{
	if(p == nil)
		return nil;
	if(s == nil){
		if(n > 0)
			sysfatal("pcarray");
		s = (uchar*)"";
	}
	if(p+n+BIT16SZ >= e)
		return nil;
	PBIT16(p, n);
	p += BIT16SZ;
	memmove(p, s, n);
	p += n;
	return p;
}

uchar*
convAI2M(AuthInfo *ai, uchar *p, int n)
{
	uchar *e = p+n;

	p = pstring(p, e, ai->cuid);
	p = pstring(p, e, ai->suid);
	p = pstring(p, e, ai->cap);
	p = pcarray(p, e, ai->secret, ai->nsecret);
	return p;
}

int
failure(Fsstate *s, char *fmt, ...)
{
	char e[ERRMAX];
	va_list arg;

	if(fmt == nil)
		rerrstr(s->err, sizeof(s->err));
	else {
		va_start(arg, fmt);
		vsnprint(e, sizeof e, fmt, arg);
		va_end(arg);
		strecpy(s->err, s->err+sizeof(s->err), e);
		werrstr(e);
	}
	flog("%d: failure %s", s->seqnum, s->err);
	return RpcFailure;
}

static int
hasqueries(Attr *a)
{
	for(; a; a=a->next)
		if(a->type == AttrQuery)
			return 1;
	return 0;
}

char *ignored[] = {
	"role",
	"disabled",
};

static int
ignoreattr(char *s)
{
	int i;

	for(i=0; i<nelem(ignored); i++)
		if(strcmp(ignored[i], s)==0)
			return 1;
	return 0;
}

Keyinfo*
mkkeyinfo(Keyinfo *k, Fsstate *fss, Attr *attr)
{
	memset(k, 0, sizeof *k);
	k->fss = fss;
	k->user = fss->sysuser;
	if(attr)
		k->attr = attr;
	else
		k->attr = fss->attr;
	return k;
}

int
findkey(Key **ret, Keyinfo *ki, char *fmt, ...)
{
	int i, s, nmatch;
	char buf[1024], *p, *who;
	va_list arg;
	Attr *a, *attr0, *attr1, *attr2, *attr3, **l;
	Key *k;

	*ret = nil;

	who = ki->user;
	attr0 = ki->attr;
	if(fmt){
		va_start(arg, fmt);
		vseprint(buf, buf+sizeof buf, fmt, arg);
		va_end(arg);
		attr1 = _parseattr(buf);
	}else
		attr1 = nil;

	if(who && strcmp(who, owner) == 0)
		who = nil;

	if(who){
		snprint(buf, sizeof buf, "owner=%q", who);
		attr2 = _parseattr(buf);
		attr3 = _parseattr("owner=*");
	}else
		attr2 = attr3 = nil;

	p = _strfindattr(attr0, "proto");
	if(p == nil)
		p = _strfindattr(attr1, "proto");
	if(p && findproto(p) == nil){
		werrstr("unknown protocol %s", p);
		_freeattr(attr1);
		_freeattr(attr2);
		_freeattr(attr3);
		return failure(ki->fss, nil);
	}

	nmatch = 0; 
	for(i=0; i<ring->nkey; i++){
		k = ring->key[i];
		if(_strfindattr(k->attr, "disabled") && !ki->usedisabled)
			continue;
		if(matchattr(attr0, k->attr, k->privattr) && matchattr(attr1, k->attr, k->privattr)){
			/* check ownership */
			if(!matchattr(attr2, k->attr, nil) && !matchattr(attr3, k->attr, nil))
				continue;
			if(nmatch++ < ki->skip)
				continue;
			if(!ki->noconf){
				switch(canusekey(ki->fss, k)){
				case -1:
					_freeattr(attr1);
					return RpcConfirm;
				case 0:
					continue;
				case 1:
					break;
				}
			}
			_freeattr(attr1);
			_freeattr(attr2);
			_freeattr(attr3);
			k->ref++;
			*ret = k;
			return RpcOk;
		}
	}
	flog("%d: no key matches %A %A %A %A", ki->fss->seqnum, attr0, attr1, attr2, attr3);
	werrstr("no key matches %A %A", attr0, attr1);
	_freeattr(attr2);
	_freeattr(attr3);
	s = RpcFailure;
	if(askforkeys && who==nil && (hasqueries(attr0) || hasqueries(attr1))){
		if(nmatch == 0){
			attr0 = _copyattr(attr0);
			for(l=&attr0; *l; l=&(*l)->next)
				;
			*l = attr1;
			for(l=&attr0; *l; ){
				if(ignoreattr((*l)->name)){
					a = *l;
					*l = (*l)->next;
					a->next = nil;
					_freeattr(a);
				}else
					l = &(*l)->next;
			}
			attr0 = sortattr(attr0);
			snprint(ki->fss->keyinfo, sizeof ki->fss->keyinfo, "%A", attr0);
			_freeattr(attr0);
			attr1 = nil;	/* attr1 was linked to attr0 */
		}else
			ki->fss->keyinfo[0] = '\0';
		s = RpcNeedkey;
	}
	_freeattr(attr1);
	if(s == RpcFailure)
		return failure(ki->fss, nil);	/* loads error string */
	return s;
}

int
findp9authkey(Key **k, Fsstate *fss)
{
	char *dom;
	Keyinfo ki;

	/*
	 * We don't use fss->attr here because we don't
	 * care about what the user name is set to, for instance.
	 */
	mkkeyinfo(&ki, fss, nil);
	ki.attr = nil;
	ki.user = nil;
	if(dom = _strfindattr(fss->attr, "dom"))
		return findkey(k, &ki, "proto=p9sk1 dom=%q role=server user?", dom);
	else
		return findkey(k, &ki, "proto=p9sk1 role=server dom? user?");
}

Proto*
findproto(char *name)
{
	int i;

	for(i=0; prototab[i]; i++)
		if(strcmp(name, prototab[i]->name) == 0)
			return prototab[i];
	return nil;
}

char*
getnvramkey(int flag, char **secstorepw)
{
	char *s;
	Nvrsafe safe;
	char spw[CONFIGLEN+1];
	int i;

	memset(&safe, 0, sizeof safe);
	/*
	 * readnvram can return -1 meaning nvram wasn't written,
	 * but safe still holds good data.
	 */
	if(readnvram(&safe, flag)<0 && safe.authid[0]==0) 
		return nil;

	/*
	 *  we're using the config area to hold the secstore
	 *  password.  if there's anything there, return it.
	 */
	memmove(spw, safe.config, CONFIGLEN);
	spw[CONFIGLEN] = 0;
	if(spw[0] != 0)
		*secstorepw = estrdup(spw);

	/*
	 *  only use nvram key if it is non-zero
	 */
	for(i = 0; i < DESKEYLEN; i++)
		if(safe.machkey[i] != 0)
			break;
	if(i == DESKEYLEN)
		return nil;

	s = emalloc(512);
	fmtinstall('H', encodefmt);
	snprint(s, 512, "key proto=p9sk1 user=%q dom=%q !hex=%.*H !password=______", 
		safe.authid, safe.authdom, DESKEYLEN, safe.machkey);
	writehostowner(safe.authid);

	return s;
}

int
isclient(char *role)
{
	if(role == nil){
		werrstr("role not specified");
		return -1;
	}
	if(strcmp(role, "server") == 0)
		return 0;
	if(strcmp(role, "client") == 0)
		return 1;
	werrstr("unknown role %q", role);
	return -1;
}

static int
hasname(Attr *a0, Attr *a1, char *name)
{
	return _findattr(a0, name) || _findattr(a1, name);
}

static int
hasnameval(Attr *a0, Attr *a1, char *name, char *val)
{
	Attr *a;

	for(a=_findattr(a0, name); a; a=_findattr(a->next, name))
		if(strcmp(a->val, val) == 0)
			return 1;
	for(a=_findattr(a1, name); a; a=_findattr(a->next, name))
		if(strcmp(a->val, val) == 0)
			return 1;
	return 0;
}

int
matchattr(Attr *pat, Attr *a0, Attr *a1)
{
	int type;

	for(; pat; pat=pat->next){
		type = pat->type;
		if(ignoreattr(pat->name))
			type = AttrDefault;
		switch(type){
		case AttrQuery:		/* name=something be present */
			if(!hasname(a0, a1, pat->name))
				return 0;
			break;
		case AttrNameval:	/* name=val must be present */
			if(!hasnameval(a0, a1, pat->name, pat->val))
				return 0;
			break;
		case AttrDefault:	/* name=val must be present if name=anything is present */
			if(hasname(a0, a1, pat->name) && !hasnameval(a0, a1, pat->name, pat->val))
				return 0;
			break;
		}
	}
	return 1;		
}

void
memrandom(void *p, int n)
{
	uchar *cp;

	for(cp = (uchar*)p; n > 0; n--)
		*cp++ = fastrand();
}

/*
 *  keep caphash fd open since opens of it could be disabled
 */
static int caphashfd;

void
initcap(void)
{
	caphashfd = open("#¤/caphash", OWRITE);
//	if(caphashfd < 0)
//		fprint(2, "%s: opening #¤/caphash: %r\n", argv0);
}

/*
 *  create a change uid capability 
 */
char*
mkcap(char *from, char *to)
{
	uchar rand[20];
	char *cap;
	char *key;
	int nfrom, nto, ncap;
	uchar hash[SHA1dlen];

	if(caphashfd < 0)
		return nil;

	/* create the capability */
	nto = strlen(to);
	nfrom = strlen(from);
	ncap = nfrom + 1 + nto + 1 + sizeof(rand)*3 + 1;
	cap = emalloc(ncap);
	snprint(cap, ncap, "%s@%s", from, to);
	memrandom(rand, sizeof(rand));
	key = cap+nfrom+1+nto+1;
	enc64(key, sizeof(rand)*3, rand, sizeof(rand));

	/* hash the capability */
	hmac_sha1((uchar*)cap, strlen(cap), (uchar*)key, strlen(key), hash, nil);

	/* give the kernel the hash */
	key[-1] = '@';
	if(write(caphashfd, hash, SHA1dlen) < 0){
		free(cap);
		return nil;
	}

	return cap;
}

int
phaseerror(Fsstate *s, char *op)
{
	char tmp[32];

	werrstr("protocol phase error: %s in state %s", op, phasename(s, s->phase, tmp));	
	return RpcPhase;
}

char*
phasename(Fsstate *fss, int phase, char *tmp)
{
	char *name;

	if(fss->phase == Broken)
		name = "Broken";
	else if(phase == Established)
		name = "Established";
	else if(phase == Notstarted)
		name = "Notstarted";
	else if(phase < 0 || phase >= fss->maxphase
	|| (name = fss->phasename[phase]) == nil){
		sprint(tmp, "%d", phase);
		name = tmp;
	}
	return name;
}

static int
outin(char *prompt, char *def, int len)
{
	char *s;

	s = readcons(prompt, def, 0);
	if(s == nil)
		return -1;
	if(s == nil)
		sysfatal("s==nil???");
	strncpy(def, s, len);
	def[len-1] = 0;
	free(s);
	return strlen(def);
}

/*
 *  get host owner and set it
 */
void
promptforhostowner(void)
{
	char owner[64], *p;

	/* hack for bitsy; can't prompt during boot */
	if(p = getenv("user")){
		writehostowner(p);
		free(p);
		return;
	}
	free(p);

	strcpy(owner, "none");
	do{
		outin("user", owner, sizeof(owner));
	} while(*owner == 0);
	writehostowner(owner);
}

char*
estrappend(char *s, char *fmt, ...)
{
	char *t;
	va_list arg;

	va_start(arg, fmt);
	t = vsmprint(fmt, arg);
	if(t == nil)
		sysfatal("out of memory");
	va_end(arg);
	s = erealloc(s, strlen(s)+strlen(t)+1);
	strcat(s, t);
	free(t);
	return s;
}


/*
 *  prompt for a string with a possible default response
 */
char*
readcons(char *prompt, char *def, int raw)
{
	int fdin, fdout, ctl, n;
	char line[10];
	char *s;

	fdin = open("/dev/cons", OREAD);
	if(fdin < 0)
		fdin = 0;
	fdout = open("/dev/cons", OWRITE);
	if(fdout < 0)
		fdout = 1;
	if(def != nil)
		fprint(fdout, "%s[%s]: ", prompt, def);
	else
		fprint(fdout, "%s: ", prompt);
	if(raw){
		ctl = open("/dev/consctl", OWRITE);
		if(ctl >= 0)
			write(ctl, "rawon", 5);
	} else
		ctl = -1;
	s = estrdup("");
	for(;;){
		n = read(fdin, line, 1);
		if(n == 0){
		Error:
			close(fdin);
			close(fdout);
			if(ctl >= 0)
				close(ctl);
			free(s);
			return nil;
		}
		if(n < 0)
			goto Error;
		if(line[0] == 0x7f)
			goto Error;
		if(n == 0 || line[0] == '\n' || line[0] == '\r'){
			if(raw){
				write(ctl, "rawoff", 6);
				write(fdout, "\n", 1);
			}
			close(ctl);
			close(fdin);
			close(fdout);
			if(*s == 0 && def != nil)
				s = estrappend(s, "%s", def);
			return s;
		}
		if(line[0] == '\b'){
			if(strlen(s) > 0)
				s[strlen(s)-1] = 0;
		} else if(line[0] == 0x15) {	/* ^U: line kill */
			if(def != nil)
				fprint(fdout, "\n%s[%s]: ", prompt, def);
			else
				fprint(fdout, "\n%s: ", prompt);
			
			s[0] = 0;
		} else {
			s = estrappend(s, "%c", line[0]);
		}
	}
}

/*
 * Insert a key into the keyring.
 * If the public attributes are identical to some other key, replace that one.
 */
int
replacekey(Key *kn, int before)
{
	int i;
	Key *k;

	for(i=0; i<ring->nkey; i++){
		k = ring->key[i];
		if(matchattr(kn->attr, k->attr, nil) && matchattr(k->attr, kn->attr, nil)){
			closekey(k);
			kn->ref++;
			ring->key[i] = kn;
			return 0;
		}
	}
	if(ring->nkey%16 == 0)
		ring->key = erealloc(ring->key, (ring->nkey+16)*sizeof(ring->key[0]));
	kn->ref++;
	if(before){
		memmove(ring->key+1, ring->key, ring->nkey*sizeof ring->key[0]);
		ring->key[0] = kn;
		ring->nkey++;
	}else
		ring->key[ring->nkey++] = kn;
	return 0;
}

char*
safecpy(char *to, char *from, int n)
{
	memset(to, 0, n);
	if(n == 1)
		return to;
	if(from==nil)
		sysfatal("safecpy called with from==nil, pc=%#p",
			getcallerpc(&to));
	strncpy(to, from, n-1);
	return to;
}

Attr*
setattr(Attr *a, char *fmt, ...)
{
	char buf[1024];
	va_list arg;
	Attr *b;

	va_start(arg, fmt);
	vseprint(buf, buf+sizeof buf, fmt, arg);
	va_end(arg);
	b = _parseattr(buf);
	a = setattrs(a, b);
	setmalloctag(a, getcallerpc(&a));
	_freeattr(b);
	return a;
}

/*
 *  add attributes in list b to list a.  If any attributes are in
 *  both lists, replace those in a by those in b.
 */
Attr*
setattrs(Attr *a, Attr *b)
{
	int found;
	Attr **l, *freea;

	for(; b; b=b->next){
		found = 0;
		for(l=&a; *l; ){
			if(strcmp(b->name, (*l)->name) == 0){
				switch(b->type){
				case AttrNameval:
					if(!found){
						found = 1;
						free((*l)->val);
						(*l)->val = estrdup(b->val);
						(*l)->type = AttrNameval;
						l = &(*l)->next;
					}else{
						freea = *l;
						*l = (*l)->next;
						freea->next = nil;
						_freeattr(freea);
					}
					break;
				case AttrQuery:
					goto continue2;
				}
			}else
				l = &(*l)->next;
		}
		if(found == 0){
			*l = _mkattr(b->type, b->name, b->val, nil);
			setmalloctag(*l, getcallerpc(&a));
		}
continue2:;
	}
	return a;		
}

void
setmalloctaghere(void *v)
{
	setmalloctag(v, getcallerpc(&v));
}

Attr*
sortattr(Attr *a)
{
	int i;
	Attr *anext, *a0, *a1, **l;

	if(a == nil || a->next == nil)
		return a;

	/* cut list in halves */
	a0 = nil;
	a1 = nil;
	i = 0;
	for(; a; a=anext){
		anext = a->next;
		if(i++%2){
			a->next = a0;
			a0 = a;
		}else{
			a->next = a1;
			a1 = a;
		}
	}

	/* sort */
	a0 = sortattr(a0);
	a1 = sortattr(a1);

	/* merge */
	l = &a;
	while(a0 || a1){
		if(a1==nil){
			anext = a0;
			a0 = a0->next;
		}else if(a0==nil){
			anext = a1;
			a1 = a1->next;
		}else if(strcmp(a0->name, a1->name) < 0){
			anext = a0;
			a0 = a0->next;
		}else{
			anext = a1;
			a1 = a1->next;
		}
		*l = anext;
		l = &(*l)->next;
	}
	*l = nil;
	return a;
}

int
toosmall(Fsstate *fss, uint n)
{
	fss->rpc.nwant = n;
	return RpcToosmall;
}

void
writehostowner(char *owner)
{
	int fd;
	char *s;

	if((s = strchr(owner,'@')) != nil){
		*s++ = 0;
		strncpy(secstore, s, (sizeof secstore)-1);
	}
	fd = open("#c/hostowner", OWRITE);
	if(fd >= 0){
		if(fprint(fd, "%s", owner) < 0)
			fprint(2, "factotum: setting #c/hostowner to %q: %r\n",
				owner);
		close(fd);
	}
}

int
attrnamefmt(Fmt *fmt)
{
	char *b, buf[1024], *ebuf;
	Attr *a;

	ebuf = buf+sizeof buf;
	b = buf;
	strcpy(buf, " ");
	for(a=va_arg(fmt->args, Attr*); a; a=a->next){
		if(a->name == nil)
			continue;
		b = seprint(b, ebuf, " %q?", a->name);
	}
	return fmtstrcpy(fmt, buf+1);
}

void
disablekey(Key *k)
{
	Attr *a;

	if(sflag)	/* not on servers */
		return;
	for(a=k->attr; a; a=a->next){
		if(a->type==AttrNameval && strcmp(a->name, "disabled") == 0)
			return;
		if(a->next == nil)
			break;
	}
	if(a)
		a->next = _mkattr(AttrNameval, "disabled", "by.factotum", nil);
	else
		k->attr = _mkattr(AttrNameval, "disabled", "by.factotum", nil);	/* not reached: always a proto attribute */
}

Bell Labs OSI certified Powered by Plan 9

(Return to Plan 9 Home Page)

Copyright © 2021 Plan 9 Foundation. All Rights Reserved.
Comments to [email protected].