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

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


#include <u.h>
#include <libc.h>
#include <fcall.h>
#include <thread.h>
#include <9p.h>
#include "cifs.h"

static char magic[] = { 0xff, 'S', 'M', 'B' };

Session *
cifsdial(char *host, char *called, char *sysname)
{
	int nbt, fd;
	char *addr;
	Session *s;

	if(Debug)
		fprint(2, "cifsdial: host=%s called=%s sysname=%s\n", host, called, sysname);

	if((addr = netmkaddr(host, "tcp", "cifs")) == nil)
		return nil;

	nbt = 0;
	if((fd = dial(addr, nil, nil, nil)) == -1){
		nbt = 1;
		if((fd = nbtdial(host, called, sysname)) == -1)
			return nil;
	}

	s = emalloc9p(sizeof(Session));
	memset(s, 0, sizeof(Session));

	s->fd = fd;
	s->nbt = nbt;
	s->mtu = MTU;
	s->pid = getpid();
	s->mid = time(nil) ^ getpid();
	s->uid = NO_UID;
	s->seq = 0;
	s->seqrun = 0;
	s->secmode = SECMODE_SIGN_ENABLED;	/* hope for the best */
	s->flags2 = FL2_KNOWS_LONG_NAMES | FL2_HAS_LONG_NAMES | FL2_PAGEING_IO;
	s->macidx = -1;

	return s;
}

void
cifsclose(Session *s)
{
	if(s->fd)
		close(s->fd);
	free(s);
}

Pkt *
cifshdr(Session *s, Share *sp, int cmd)
{
	Pkt *p;
	int sign, tid, dfs;

	dfs = 0;
	tid = NO_TID;
	Active = IDLE_TIME;
	werrstr("");
	sign = s->secmode & SECMODE_SIGN_ENABLED? FL2_PACKET_SIGNATURES: 0;

	if(sp){
		tid = sp->tid;
// FIXME!		if(sp->options & SMB_SHARE_IS_IN_DFS)
// FIXME!			dfs = FL2_DFS;
	}

	p = emalloc9p(sizeof(Pkt) + MTU);
	memset(p, 0, sizeof(Pkt) +MTU);

	p->buf = (uchar *)p + sizeof(Pkt);
	p->s = s;

	qlock(&s->seqlock);
	if(s->seqrun){
		p->seq = s->seq;
		s->seq = (s->seq + 2) % 0x10000;
	}
	qunlock(&s->seqlock);

	nbthdr(p);
	pmem(p, magic, nelem(magic));
	p8(p, cmd);
	pl32(p, 0);				/* status (error) */
	p8(p, FL_CASELESS_NAMES | FL_CANNONICAL_NAMES); /* flags */
	pl16(p, s->flags2 | dfs | sign);	/* flags2 */
	pl16(p, (s->pid >> 16) & 0xffff);	/* PID MS bits */
	pl32(p, p->seq);			/* MAC / sequence number */
	pl32(p, 0);				/* MAC */
	pl16(p, 0);				/* padding */

	pl16(p, tid);
	pl16(p, s->pid & 0xffff);
	pl16(p, s->uid);
	pl16(p, s->mid);

	p->wordbase = p8(p, 0);		/* filled in by pbytes() */

	return p;
}

void
pbytes(Pkt *p)
{
	int n;

	assert(p->wordbase != nil);	/* cifshdr not called */
	assert(p->bytebase == nil);	/* called twice */

	n = p->pos - p->wordbase;
	assert(n % 2 != 0);		/* even addr */
	*p->wordbase = n / 2;

	p->bytebase = pl16(p, 0);	/* filled in by cifsrpc() */
}

static void
dmp(int seq, uchar *buf)
{
	int i;

	if(seq == 99)
		print("\n   ");
	else
		print("%+2d ", seq);
	for(i = 0; i < 8; i++)
		print("%02x ", buf[i] & 0xff);
	print("\n");
}

int
cifsrpc(Pkt *p)
{
	int flags2, got, err;
	uint tid, uid, seq;
	uchar *pos;
	char m[nelem(magic)];

	pos = p->pos;
	if(p->bytebase){
		p->pos = p->bytebase;
		pl16(p, pos - (p->bytebase + 2)); /* 2 = sizeof bytecount */
	}
	p->pos = pos;

	if(p->s->secmode & SECMODE_SIGN_ENABLED)
		macsign(p, p->seq);

	qlock(&p->s->rpclock);
	got = nbtrpc(p);
	qunlock(&p->s->rpclock);
	if(got == -1)
		return -1;

	gmem(p, m, nelem(magic));
	if(memcmp(m, magic, nelem(magic)) != 0){
		werrstr("cifsrpc: bad magic number in packet %20ux%02ux%02ux%02ux",
			m[0], m[1], m[2], m[3]);
		return -1;
	}

	g8(p);				/* cmd */
	err = gl32(p);			/* errcode */
	g8(p);				/* flags */
	flags2 = gl16(p);		/* flags2 */
	gl16(p);			/* PID MS bits */
	seq = gl32(p);			/* reserved */
	gl32(p);			/* MAC (if in use) */
	gl16(p);			/* Padding */
	tid = gl16(p);			/* TID */
	gl16(p);			/* PID lsbs */
	uid = gl16(p);			/* UID */
	gl16(p);			/* mid */
	g8(p);				/* word count */

	if(p->s->secmode & SECMODE_SIGN_ENABLED){
		if(macsign(p, p->seq+1) != 0 && p->s->seqrun){
			werrstr("cifsrpc: invalid packet signature");
print("MAC signature bad\n");
// FIXME: for debug only			return -1;
		}
	}else{
		/*
		 * We allow the sequence number of zero as some old samba
		 * servers seem to fall back to this unexpectedly
		 * after reporting sequence numbers correctly for a while.
		 *
		 * Some other samba servers seem to always report a sequence
		 * number of zero if MAC signing is disabled, so we have to
		 * catch that too.
		 */
		if(p->s->seqrun && seq != p->seq && seq != 0){
			print("%ux != %ux bad sequence number\n", seq, p->seq);
			return -1;
		}
	}

	p->tid = tid;
	if(p->s->uid == NO_UID)
		p->s->uid = uid;

	if(flags2 & FL2_NT_ERRCODES){
		/* is it a real error rather than info/warning/chatter? */
		if((err & 0xF0000000) == 0xC0000000){
			werrstr("%s", nterrstr(err));
			return -1;
		}
	}else{
		if(err){
			werrstr("%s", doserrstr(err));
			return -1;
		}
	}
	return got;
}


/*
 * Some older servers (old samba) prefer to talk older
 * dialects but if given no choice they will talk the
 * more modern ones, so we don't give them the choice.
 */
int
CIFSnegotiate(Session *s, long *svrtime, char *domain, int domlen, char *cname,
	int cnamlen)
{
	int d, i;
	char *ispeak = "NT LM 0.12";
	static char *dialects[] = {
//		{ "PC NETWORK PROGRAM 1.0"},
//		{ "MICROSOFT NETWORKS 1.03"},
//		{ "MICROSOFT NETWORKS 3.0"},
//		{ "LANMAN1.0"},
//		{ "LM1.2X002"},
//		{ "NT LANMAN 1.0"},
		{ "NT LM 0.12" },
	};
	Pkt *p;

	p = cifshdr(s, nil, SMB_COM_NEGOTIATE);
	pbytes(p);
	for(i = 0; i < nelem(dialects); i++){
		p8(p, STR_DIALECT);
		pstr(p, dialects[i]);
	}

	if(cifsrpc(p) == -1){
		free(p);
		return -1;
	}

	d = gl16(p);
	if(d < 0 || d > nelem(dialects)){
		werrstr("no CIFS dialect in common");
		free(p);
		return -1;
	}

	if(strcmp(dialects[d], ispeak) != 0){
		werrstr("%s dialect unsupported", dialects[d]);
		free(p);
		return -1;
	}

	s->secmode = g8(p);			/* Security mode */

	gl16(p);				/* Max outstanding requests */
	gl16(p);				/* Max VCs */
	s->mtu = gl32(p);			/* Max buffer size */
	gl32(p);				/* Max raw buffer size (depricated) */
	gl32(p);				/* Session key */
	s->caps = gl32(p);			/* Server capabilities */
	*svrtime = gvtime(p);			/* fileserver time */
	s->tz = (short)gl16(p) * 60; /* TZ in mins, is signed (SNIA doc is wrong) */
	s->challen = g8(p);			/* Encryption key length */
	gl16(p);
	gmem(p, s->chal, s->challen);		/* Get the challenge */
	gstr(p, domain, domlen);		/* source domain */

	{		/* NetApp Filer seem not to report its called name */
		char *cn = emalloc9p(cnamlen);

		gstr(p, cn, cnamlen);		/* their name */
		if(strlen(cn) > 0)
			memcpy(cname, cn, cnamlen);
		free(cn);
	}

	if(s->caps & CAP_UNICODE)
		s->flags2 |= FL2_UNICODE;

	free(p);
	return 0;
}

int
CIFSsession(Session *s)
{
	char os[64], *q;
	Rune r;
	Pkt *p;
	enum {
		mycaps = CAP_UNICODE | CAP_LARGE_FILES | CAP_NT_SMBS |
			CAP_NT_FIND | CAP_STATUS32,
	};

	s->seqrun = 1;	/* activate the sequence number generation/checking */

	p = cifshdr(s, nil, SMB_COM_SESSION_SETUP_ANDX);
	p8(p, 0xFF);			/* No secondary command */
	p8(p, 0);			/* Reserved (must be zero) */
	pl16(p, 0);			/* Offset to next command */
	pl16(p, MTU);			/* my max buffer size */
	pl16(p, 1);			/* my max multiplexed pending requests */
	pl16(p, 0);			/* Virtual connection # */
	pl32(p, 0);			/* Session key (if vc != 0) */


	if((s->secmode & SECMODE_PW_ENCRYPT) == 0) {
		pl16(p, utflen(Sess->auth->resp[0])*2 + 2); /* passwd size */
		pl16(p, utflen(Sess->auth->resp[0])*2 + 2); /* passwd size (UPPER CASE) */
		pl32(p, 0);			/* Reserved */
		pl32(p, mycaps);
		pbytes(p);

		for(q = Sess->auth->resp[0]; *q; ){
			q += chartorune(&r, q);
			pl16(p, toupperrune(r));
		}
		pl16(p, 0);

		for(q = Sess->auth->resp[0]; *q; ){
			q += chartorune(&r, q);
			pl16(p, r);
		}
		pl16(p, 0);
	}else{
		pl16(p, Sess->auth->len[0]);	/* LM passwd size */
		pl16(p, Sess->auth->len[1]);	/* NTLM passwd size */
		pl32(p, 0);			/* Reserved  */
		pl32(p, mycaps);
		pbytes(p);

		pmem(p, Sess->auth->resp[0], Sess->auth->len[0]);
		pmem(p, Sess->auth->resp[1], Sess->auth->len[1]);
	}

	pstr(p, Sess->auth->user);	/* Account name */
	pstr(p, Sess->auth->windom);	/* Primary domain */
	pstr(p, "plan9");		/* Client OS */
	pstr(p, argv0);			/* Client LAN Manager type */

	if(cifsrpc(p) == -1){
		free(p);
		return -1;
	}

	g8(p);				/* Reserved (0) */
	gl16(p);			/* Offset to next command wordcount */
	Sess->isguest = gl16(p) & 1;	/* logged in as guest */

	gl16(p);
	gl16(p);
	/* no security blob here - we don't understand extended security anyway */
	gstr(p, os, sizeof(os));
	s->remos = estrdup9p(os);

	free(p);
	return 0;
}


CIFStreeconnect(Session *s, char *cname, char *tree, Share *sp)
{
	int len;
	char *resp, *path;
	char zeros[24];
	Pkt *p;

	resp = Sess->auth->resp[0];
	len  = Sess->auth->len[0];
	if((s->secmode & SECMODE_USER) != SECMODE_USER){
		memset(zeros, 0, sizeof(zeros));
		resp = zeros;
		len = sizeof(zeros);
	}

	p = cifshdr(s, nil, SMB_COM_TREE_CONNECT_ANDX);
	p8(p, 0xFF);			/* Secondary command */
	p8(p, 0);			/* Reserved */
	pl16(p, 0);			/* Offset to next Word Count */
	pl16(p, 0);			/* Flags */

	if((s->secmode & SECMODE_PW_ENCRYPT) == 0){
		pl16(p, len+1);		/* password len, including null */
		pbytes(p);
		pascii(p, resp);
	}else{
		pl16(p, len);
		pbytes(p);
		pmem(p, resp, len);
	}

	path = smprint("//%s/%s", cname, tree);
	strupr(path);
	ppath(p, path);			/* path */
	free(path);

	pascii(p, "?????");	/* service type any (so we can do RAP calls) */

	if(cifsrpc(p) == -1){
		free(p);
		return -1;
	}
	g8(p);				/* Secondary command */
	g8(p);				/* Reserved */
	gl16(p);			/* Offset to next command */
	sp->options = g8(p);		/* options supported */
	sp->tid = p->tid;		/* get received TID from packet header */
	free(p);
	return 0;
}

int
CIFSlogoff(Session *s)
{
	int rc;
	Pkt *p;

	p = cifshdr(s, nil, SMB_COM_LOGOFF_ANDX);
	p8(p, 0xFF);			/* No ANDX command */
	p8(p, 0);			/* Reserved (must be zero) */
	pl16(p, 0);			/* offset ot ANDX */
	pbytes(p);
	rc = cifsrpc(p);

	free(p);
	return rc;
}

int
CIFStreedisconnect(Session *s, Share *sp)
{
	int rc;
	Pkt *p;

	p = cifshdr(s, sp, SMB_COM_TREE_DISCONNECT);
	pbytes(p);
	rc = cifsrpc(p);

	free(p);
	return rc;
}


int
CIFSdeletefile(Session *s, Share *sp, char *name)
{
	int rc;
	Pkt *p;

	p = cifshdr(s, sp, SMB_COM_DELETE);
	pl16(p, ATTR_HIDDEN|ATTR_SYSTEM);	/* search attributes */
	pbytes(p);
	p8(p, STR_ASCII);			/* buffer format */
	ppath(p, name);
	rc = cifsrpc(p);

	free(p);
	return rc;
}

int
CIFSdeletedirectory(Session *s, Share *sp, char *name)
{
	int rc;
	Pkt *p;

	p = cifshdr(s, sp, SMB_COM_DELETE_DIRECTORY);
	pbytes(p);
	p8(p, STR_ASCII);		/* buffer format */
	ppath(p, name);
	rc = cifsrpc(p);

	free(p);
	return rc;
}

int
CIFScreatedirectory(Session *s, Share *sp, char *name)
{
	int rc;
	Pkt *p;

	p = cifshdr(s, sp, SMB_COM_CREATE_DIRECTORY);
	pbytes(p);
	p8(p, STR_ASCII);
	ppath(p, name);
	rc = cifsrpc(p);

	free(p);
	return rc;
}

int
CIFSrename(Session *s, Share *sp, char *old, char *new)
{
	int rc;
	Pkt *p;

	p = cifshdr(s, sp, SMB_COM_RENAME);
	pl16(p, ATTR_HIDDEN|ATTR_SYSTEM|ATTR_DIRECTORY); /* search attributes */
	pbytes(p);
	p8(p, STR_ASCII);
	ppath(p, old);
	p8(p, STR_ASCII);
	ppath(p, new);
	rc = cifsrpc(p);

	free(p);
	return rc;
}


/* for NT4/Win2k/XP */
int
CIFS_NT_opencreate(Session *s, Share *sp, char *name, int flags, int options,
	int attrs, int access, int share, int action, int *result, FInfo *fi)
{
	Pkt *p;
	int fh;

	p = cifshdr(s, sp, SMB_COM_NT_CREATE_ANDX);
	p8(p, 0xFF);			/* Secondary command */
	p8(p, 0);			/* Reserved */
	pl16(p, 0);			/* Offset to next command */
	p8(p, 0);			/* Reserved */
	pl16(p, utflen(name) *2);	/* file name len */
	pl32(p, flags);			/* Flags */
	pl32(p, 0);			/* fid of cwd, if relative path */
	pl32(p, access);		/* access desired */
	pl64(p, 0);			/* initial allocation size */
	pl32(p, attrs);			/* Extended attributes */
	pl32(p, share);			/* Share Access */
	pl32(p, action);		/* What to do on success/failure */
	pl32(p, options);		/* Options */
	pl32(p, SECURITY_IMPERSONATION); /* Impersonation level */
	p8(p, SECURITY_CONTEXT_TRACKING | SECURITY_EFFECTIVE_ONLY); /* security flags */
	pbytes(p);
	p8(p, 0);			/* FIXME: padding? */
	ppath(p, name);			/* filename */

	if(cifsrpc(p) == -1){
		free(p);
		return -1;
	}

	memset(fi, 0, sizeof(FInfo));
	g8(p);				/* Secondary command */
	g8(p);				/* Reserved */
	gl16(p);			/* Offset to next command */
	g8(p);				/* oplock granted */
	fh = gl16(p);			/* FID for opened object */
	*result = gl32(p);		/* create action taken */
	gl64(p);			/* creation time */
	fi->accessed = gvtime(p);	/* last access time */
	fi->written = gvtime(p);	/* last written time */
	fi->changed = gvtime(p);	/* change time */
	fi->attribs = gl32(p);		/* extended attributes */
	gl64(p);			/* bytes allocated */
	fi->size = gl64(p);		/* file size */

	free(p);
	return fh;
}

/* for Win95/98/ME */
CIFS_SMB_opencreate(Session *s, Share *sp, char *name, int access,
	int attrs, int action, int *result)
{
	Pkt *p;
	int fh;

	p = cifshdr(s, sp, SMB_COM_OPEN_ANDX);
	p8(p, 0xFF);			/* Secondary command */
	p8(p, 0);			/* Reserved */
	pl16(p, 0);			/* Offset to next command */
	pl16(p, 0);			/* Flags (0 == no stat(2) info) */
	pl16(p, access);		/* desired access */
	pl16(p, ATTR_HIDDEN|ATTR_SYSTEM);/* search attributes */
	pl16(p, attrs);			/* file attribytes */
	pdatetime(p, 0);		/* creation time (0 == now) */
	pl16(p, action);		/* What to do on success/failure */
	pl32(p, 0);			/* allocation size */
	pl32(p, 0);			/* reserved */
	pl32(p, 0);			/* reserved */
	pbytes(p);
	ppath(p, name);			/* filename */

	if(cifsrpc(p) == -1){
		free(p);
		return -1;
	}

	g8(p);				/* Secondary command */
	g8(p);				/* Reserved */
	gl16(p);			/* Offset to next command */
	fh = gl16(p);			/* FID for opened object */
	gl16(p);			/* extended attributes */
	gvtime(p);			/* last written time */
	gl32(p);			/* file size */
	gl16(p);			/* file type (disk/fifo/printer etc) */
	gl16(p);			/* device status (for fifos) */
	*result = gl16(p);		/* access granted */

	free(p);
	return fh;
}

vlong
CIFSwrite(Session *s, Share *sp, int fh, uvlong off, void *buf, vlong n)
{
	Pkt *p;
	vlong got;

	/* FIXME: Payload should be padded to long boundary */
	assert((n   & 0xffffffff00000000LL) == 0 || s->caps & CAP_LARGE_FILES);
	assert((off & 0xffffffff00000000LL) == 0 || s->caps & CAP_LARGE_FILES);
	assert(n < s->mtu - T2HDRLEN || s->caps & CAP_LARGE_WRITEX);

	p = cifshdr(s, sp, SMB_COM_WRITE_ANDX);
	p8(p, 0xFF);			/* Secondary command */
	p8(p, 0);			/* Reserved */
	pl16(p, 0);			/* Offset to next command */
	pl16(p, fh);			/* File handle */
	pl32(p, off & 0xffffffff);	/* LSBs of Offset */
	pl32(p, 0);			/* Reserved (0) */
	pl16(p, s->nocache);		/* Write mode (0 - write through) */
	pl16(p, 0);			/* Bytes remaining */
	pl16(p, n >> 16);		/* MSBs of length */
	pl16(p, n & 0xffffffff);	/* LSBs of length */
	pl16(p, T2HDRLEN);		/* Offset to data, in bytes */
	pl32(p, off >> 32);		/* MSBs of offset */
	pbytes(p);

	p->pos = p->buf +T2HDRLEN +NBHDRLEN;
	pmem(p, buf, n);		/* Data */

	if(cifsrpc(p) == -1){
		free(p);
		return -1;
	}

	g8(p);				/* Secondary command */
	g8(p);				/* Reserved */
	gl16(p);			/* Offset to next command */
	got = gl16(p);			/* LSWs of bytes written */
	gl16(p);			/* remaining (space ?) */
	got |= (gl16(p) << 16);		/* MSWs of bytes written */

	free(p);
	return got;
}

vlong
CIFSread(Session *s, Share *sp, int fh, uvlong off, void *buf, vlong n,
	vlong minlen)
{
	int doff;
	vlong got;
	Pkt *p;

	assert((n   & 0xffffffff00000000LL) == 0 || s->caps & CAP_LARGE_FILES);
	assert((off & 0xffffffff00000000LL) == 0 || s->caps & CAP_LARGE_FILES);
	assert(n < s->mtu - T2HDRLEN || s->caps & CAP_LARGE_READX);

	p = cifshdr(s, sp, SMB_COM_READ_ANDX);
	p8(p, 0xFF);			/* Secondary command */
	p8(p, 0);			/* Reserved */
	pl16(p, 0);			/* Offset to next command */
	pl16(p, fh);			/* File handle */
	pl32(p, off & 0xffffffff);	/* Offset to beginning of write */
	pl16(p, n);			/* Maximum number of bytes to return */
	pl16(p, minlen);		/* Minimum number of bytes to return */
	pl32(p, (uint)n >> 16);		/* MSBs of maxlen */
	pl16(p, 0);			/* Bytes remaining to satisfy request */
	pl32(p, off >> 32);		/* MS 32 bits of offset */
	pbytes(p);

	if(cifsrpc(p) == -1){
		free(p);
		return -1;
	}

	g8(p);				/* Secondary command */
	g8(p);				/* Reserved */
	gl16(p);			/* Offset to next command */
	gl16(p);			/* Remaining */
	gl16(p);			/* Compression mode */
	gl16(p);			/* Reserved */
	got = gl16(p);			/* length */
	doff = gl16(p);			/* Offset from header to data */
	got |= gl16(p) << 16;

	p->pos = p->buf + doff + NBHDRLEN;

	gmem(p, buf, got);		 /* data */
	free(p);
	return got;
}

int
CIFSflush(Session *s, Share *sp, int fh)
{
	int rc;
	Pkt *p;

	p = cifshdr(s, sp, SMB_COM_FLUSH);
	pl16(p, fh);			/* fid */
	pbytes(p);
	rc = cifsrpc(p);

	free(p);
	return rc;
}

/*
 * Setting the time of last write to -1 gives "now" if the file
 * was written and leaves it the same if the file wasn't written.
 */
int
CIFSclose(Session *s, Share *sp, int fh)
{
	int rc;
	Pkt *p;

	p = cifshdr(s, sp, SMB_COM_CLOSE);
	pl16(p, fh);			/* fid */
	pl32(p, ~0L);			/* Time of last write (none) */
	pbytes(p);
	rc = cifsrpc(p);

	free(p);
	return rc;
}


int
CIFSfindclose2(Session *s, Share *sp, int sh)
{
	int rc;
	Pkt *p;

	p = cifshdr(s, sp, SMB_COM_FIND_CLOSE2);
	pl16(p, sh);			/* sid */
	pbytes(p);
	rc = cifsrpc(p);

	free(p);
	return rc;
}


int
CIFSecho(Session *s)
{
	Pkt *p;
	int rc;

	p = cifshdr(s, nil, SMB_COM_ECHO);
	pl16(p, 1);				/* number of replies */
	pbytes(p);
	pascii(p, "abcdefghijklmnopqrstuvwxyz"); /* data */

	rc = cifsrpc(p);
	free(p);
	return rc;
}


int
CIFSsetinfo(Session *s, Share *sp, char *path, FInfo *fip)
{
	int rc;
	Pkt *p;

	p = cifshdr(s, sp, SMB_COM_SET_INFORMATION);
	pl16(p, fip->attribs);
	pl32(p, time(nil) - s->tz);	/* modified time */
	pl64(p, 0);			/* reserved */
	pl16(p, 0);			/* reserved */

	pbytes(p);
	p8(p, STR_ASCII);		/* buffer format */
	ppath(p, path);

	rc = cifsrpc(p);
	free(p);
	return rc;
}

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].