27 entries in /:

fun with SCM_RIGHTS

One feature of unix domain sockets I always wanted to try is the ability to pass file descriptors to another process. The Linux unix(7) man page makes this sound like a simple procedure but as always the devil is in the details.

In the application I have in mind I want to use this feature to redirect the standard input, standard output and standard error of one process into a virtual terminal of screen. The redirected process will later be init and has to have PID 1. Therefore I can't simply start it in screen itself. The way I imagine this is starting screen in the background with a terminal_server in one virtual terminal. My init process then connects to this server receives the file descriptors for standard input, standard output and standard output and redirects its own input, output and error there. It then exec()s the real init. This will at one point make it possible to interact with the boot process from an SSH connection on servers that have no serial console.

To make the following examples more readable I leave out the error handling. I also use C99 to declare variables where I need them to make the examples more readable. Both client and server need the following includes:

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

#include <sys/types.h>
#include <sys/socket.h>
#include <sys/un.h>

terminal_server.c

First the server opens a new unix domain socket(2), assigns a name to it using bind(2) and listen(2)s on it:

int server_socket = socket(PF_UNIX, SOCK_STREAM, 0);

struct sockaddr_un server_address = { AF_UNIX, "\0init_console" };
bind(server_socket, (struct sockaddr *) &server_address, sizeof server_address);
listen(server_socket, 1);

As you can see the path name starts with a zero byte. This denotes an abstract domain socket that is independent of the file system.

Next the server waits for the client to connect:

struct sockaddr_un client_address;
socklen_t client_address_length = sizeof client_address;
int client_connection = accept(server_socket,
                               (struct sockaddr *) &client_address,
                               &client_address_length);
close(server_socket);

The accept(2) call blocks until the client connects. When the client is connected the server socket can be closed.

Now that there is a connection in the server to the client, the message containing the file descriptors needs to be prepared. This message will then be send with the sendmsg(2) system call:

sendmsg(client_connection, &message, 0);

Preparing the message is the most difficult part of the whole process. First we need the file descriptors in an array and a buffer:

int file_descriptors[3] = { STDIN_FILENO, STDOUT_FILENO, STDERR_FILENO };
char buffer[CMSG_SPACE(sizeof file_descriptors)];

This buffer needs to be stored in in a new msghdr structure:

struct msghdr message = {
  .msg_control = buffer,
  .msg_controllen = sizeof buffer,
};

Then the data can be set describing the operation we want to perform:

struct cmsghdr *cmessage = CMSG_FIRSTHDR(&message);
cmessage->cmsg_level = SOL_SOCKET;
cmessage->cmsg_type = SCM_RIGHTS;
cmessage->cmsg_len = CMSG_LEN(sizeof file_descriptors);
message.msg_controllen = cmessage->cmsg_len;

Copy the file descriptor data into the buffer:

memcpy(CMSG_DATA(cmessage), file_descriptors, sizeof file_descriptors);

After performing this preparations the sendmsg(2) should work as expected. Unfortunately it doesn't. We only sent so-called ancillary data and actual data and this doesn't seem to work on Linux.

Therefore we add one byte of phony data:

char ping = 23;
struct iovec ping_vec = {
  .iov_base = &ping,
  .iov_len = sizeof ping,
};

message.msg_iov = &ping_vec,
message.msg_iovlen = 1,

Would've also been possible to send the number of file descriptors we want to transfer but this value is a constant here.

After the message has been successfully sent the server has to do nothing else to do. It can close the client connection and its standard input, output and error file descriptors:

close(client_connection);

close(STDIN_FILENO);
close(STDOUT_FILENO);
close(STDERR_FILENO);

To prevent the screen terminal from closing the server needs to enter a sleeping state. This can be done with the pause(2) system call:

pause();

terminal_client.c

The client is much easier than the server. The first step is to connect to the server:

struct sockaddr_un server_address = { AF_UNIX, "\0init_console" };
int client_socket = socket(PF_UNIX, SOCK_STREAM, 0);
connect(client_socket, (struct sockaddr *) &server_address, sizeof server_address);

Then the buffer for the message that will be received needs to be set up. We already know that we need to receive one phony byte so we can express this more concise:

int file_descriptors[3];
char buffer[CMSG_SPACE(sizeof file_descriptors)];

char ping;
struct iovec ping_vec = {
  .iov_base = &ping,
  .iov_len = sizeof ping,
};

struct msghdr message = {
  .msg_control = buffer,
  .msg_controllen = sizeof buffer,
  .msg_iov = &ping_vec,
  .msg_iovlen = 1,
};

Then we can wait for the message:

recvmsg(client_socket, &message, 0) < 0);

recvmsg(2) blocks until a message with the specified length is received. Without the phony data byte this call won't return even if sendmsg(2) at the server side succeeds.

The connection is no longer needed and can be closed:

close(client_socket);

At this point the client already has received the file descriptors. To find out their numbers the message needs to be parsed. Next the message needs to be parsed:

struct cmsghdr *cmessage = CMSG_FIRSTHDR(&message);
memcpy(file_descriptors, CMSG_DATA(cmessage), sizeof file_descriptors);

Now the file descriptors can be redirected:

dup2(file_descriptors[0], STDIN_FILENO);
close(file_descriptors[0]);
dup2(file_descriptors[1], STDOUT_FILENO);
close(file_descriptors[1]);
dup2(file_descriptors[2], STDERR_FILENO);
close(file_descriptors[2]);

The dup2(2) system call redirects one file descriptor to another. It's very important not to accidentally swap the arguments as this can lead to undesired effects.

Now that everything is set up the client can be replaced by the program that should be run in the terminal of the server. For testing purposes a simple shell is enough:

execl("/bin/sh", "/bin/sh", NULL)

If you made it this far I hope you found this post interesting and informative. If you want me to put both programs up in a form that can be readily compiled drop me a line via XMPP or mail.


What's my rsync doing?

#!/bin/bash
for pid in $(pidof rsync)
do
        for link in /proc/${pid}/fd/*
        do
                path=$(readlink "${link}")
                if grep -q -v -E '^(socket:|pipe:|/dev)' <<<"${path}" ; then
                        ls -sh1d "${path}"
                fi
        done
done

fun with dlsym

Prerequisites:

typedef int (*hook_fn)(void **);
hook_fn f;

A problem that often occurs when using dlsym is that it returns a pointer to void when you need a function pointer. In ISO C and C++ it is forbidden to convert between both:

f = dlsym(h, "hook");

When compiling C gcc you'll get the following warning: ISO C forbids initialization between function pointer and ‘void *’. g++ makes this even an error: invalid conversion from ‘void*’ to ‘int (*)(void**)’. After adding the needed cast there still remains a warning: ISO C++ forbids casting between pointer-to-function and pointer-to-object.

A common workaround is to trick the compiler by casting the location of the pointer you want to assign to to void **:

*(void **)(&f) = dlsym(h, "hook");

This shuts up gcc but with g++ you'll eventually get the following warning: dereferencing type-punned pointer will break strict-aliasing rules. I don't want to be distracted by those warnings and I tried to think of another way to get rid of them. My solution now is to cast dlsym to return the function type I want:

typedef hook_fn (*hook_dlsym_t)(void *, const char *);
f = ((hook_dlsym_t)(dlsym))(h, "hook");

hardlinkpysqlite3

I'm using ccollect for my backups. It works really well and I especially like its use of rsync's hardlink feature. That way only the files that are changed take space while still fully working directory snapshots. Sometimes, however, I still wind up with another full copy.

Gentoo has a package for a tool called hardlink++. On his site the author states he doesn't have the source code anymore because he rewrote it in Python. The new version is called hardlinkpy. I looked at it and it seems to have quite a few inefficiencies: it does byte-by-byte comparison for files and it compares each file to every other file that may match, i.e. has the attributes. It also prints out every step it does and it's impossible to switch this off because of a programming error:

diff --git a/hardlink.py b/hardlink.py
index 0399752..89ab144 100755
--- a/hardlink.py
+++ b/hardlink.py
@@ -415,7 +415,7 @@ def parseCommandLine():

     parser.add_option("-v", "--verbose",
         help="Verbosity level (default: %default)", metavar="LEVEL",
-        action="store", dest="verbose", default=1,)
+        type="int", dest="verbose", default=1,)

     parser.add_option("-x", "--exclude",
         help="Regular expression used to exclude files/dirs (may specify multiple times)", metavar="REGEX",

Anyway I thought I could do it better. Instead of comparing bytewise my script only compares hashes (sha256 by default). It also gathers data first and only tries to find duplicates later instead of doing both at the same time like hardlinkpy. To store the data about files and find the duplicates later I decided to use a memory based SQLite database. That way it's relatively easy to specify what attributes should be checked. I guess it's also easier to extend. My roommate asked me for script that checks his mp3 database for duplicates by comparing ID3 tags. Maybe I'll be able to prove this theory.

I found that both scripts perfomed quite differently, depending on the workload, the running time really depended on the workload.

I have a test script that creates a directory with many small files. It then creates another directory by recursively copying over every file:

mkdir "$a"

for i in `seq 1 100` ; do
    mkdir "$a/$i"
    for j in `seq 1 100` ; do
        echo "$i$j" > "$a/$i/$j"
    done
done

cp -r "$a" "$b"

Running my script or hardlinkpy over the two directories should cut down the space used by the files in the second one to zero. The directories still take space, but that's the smaller part. Before:

# du -sh a b
40M     a
40M     b

After:

du -sh a b
37M     a
404K    b

Running hardlinkpy took relatively long, about 30 minutes. My script only took about 7 minutes. This was a worst-case scenario for hardlinkpy because the files are very similar in size and thus it ends up doing many, many comparisons. A more realistic scenario, comparing the source trees for Linux 2.6.20 and Linux 2.6.21 shows quite a different picture: my script taking 18 minutes and hardlinkpy about 10 seconds! I'm still trying to find out what's the reason for this. I may be able to tune the SQL part a little bit more and make it only create digests when it's neccessary. It's very interesting, however, to find out how much space one can save by running any of the scripts over the /usr/src directory:

# before
273M    linux-2.6.20
278M    linux-2.6.21
# after
272M    linux-2.6.20
127M    linux-2.6.21

If anyone is interested, the source code is available in a Git repository at the following location:

http://cthulhu.c3d2.de/~toidinamai/git/hardlinkpysqlite3.git

It requires Python 2.5 at the moment because of the use of the sqlite3 module and hashlib.


fgets

The fgets function seems to have different semantics on different systems after an EOF condition under Linux (glibc) and FreeBSD. Under Linux you can happily call fgets twice in a row and it will Just Work™. Under FreeBSD, you'll have to say "Sorry!", i.e. clearerr, and it won't talk to you unless you do it. I haven't yet found out where this happens but I find this behavior rather annoying. The worst part is that this doesn't seem documented.

The following program illustrates the problem. Press Ctrl-D at the first prompt. Linux will try to read again the second time. FreeBSD won't unless you put a clearerr in there.

#include <stdio.h>

int main(int argc, const char **argv) {
  char buffer[256];
  fputs("What? ", stdout); fflush(stdout);
  if (fgets(buffer, sizeof buffer, stdin) != NULL) {
    fprintf(stdout, "Got 1: '%s'\n", buffer);
  }
  fputs("What? ", stdout); fflush(stdout);
  if (fgets(buffer, sizeof buffer, stdin) != NULL) {
    fprintf(stdout, "Got 2: '%s'\n", buffer);
  }
  return 0;
}

vlock 2 now FreeBSD compatible

vlock-2 is now FreeBSD compatible out of the box. Please test:

cg-clone http://cthulhu.c3d2.de/~toidinamai/git/vlock.git#vlock-2
cd vlock
gmake PAM_LIBS='-lpam'
# as root
gmake ROOT_GROUP=wheel VLOCK_GROUP=wheel INSTALL=ginstall install

You'll need ginstall (coreutils) for the installation. Porting was rather straight forward as console ioctls are very similar. I found FreeBSD kernel source more difficult to read, though. One example:

Linux (drivers/char/vt_ioctl.c)

/*
 * ioctl(fd, VT_ACTIVATE, num) will cause us to switch to vt # num,
 * with num >= 1 (switches to vt 0, our console, are not allowed, just
 * to preserve sanity).
 */
case VT_ACTIVATE:
  if (!perm)
    return -EPERM;
  if (arg == 0 || arg > MAX_NR_CONSOLES)
    return -ENXIO;
  arg--;
  acquire_console_sem();
  i = vc_allocate(arg);
  release_console_sem();
  if (i)
    return i;
  set_console(arg);
  return 0;

FreeBSD (sys/dev/syscons/syscons.c)

case VT_ACTIVATE:           /* switch to screen *data */
    i = (*(int *)data == 0) ? scp->index : (*(int *)data - 1);
    s = spltty();
    error = sc_clean_up(sc->cur_scp);
    splx(s);
    if (error)
        return error;
    return sc_switch_scr(sc, i);

vlock 2.0 alpha1

I just tagged and uploaded vlock 2.0 alpha1. You can download it from http://cthulhu.c3d2.de/~toidinamai/vlock/archive/testing/ or grab the vlock-2-release branch from http://cthulhu.c3d2.de/~toidinamai/git/vlock.git with something like

cg-clone http://cthulhu.c3d2.de/~toidinamai/git/vlock.git#vlock-2-release

On the user interface and functionality side not much has changed for the casual observer. Codewise this release is a major rewrite of vlock sharing almost no code with previous versions. The reason for this is that I wanted more configurability and hackability while preserving and improving ease of maintenance. To achieve that goal I split up vlock into separate programs each fullfilling one function. They are:

vlock-grab
This program allocates a new virtual terminal and run the program given as its arguments there. While the program is running it prevents switching away from the console. vlock-grab itself must be installed as a setuid root binary (unless a system's administrator has a very elaborate permission scheme). However the program it starts is run as the user who started vlock-grab.
vlock-auth
This program blocks its terminal by looping until it gets proper authentification data. It ignores certain signals such as SIGINT (^C), SIGQUIT (^\), SIGTSTP (^Z) and SIGHUP. It pretty much amounts to the same as running vlock with the --current option. While may be possible on modern linux distributions to install this program without the setuid bit set it is recommended and should be safe not do so.
vlock-nosysrq
This program turns off the Linux SysRq option through sysctl and runs another program just like vlock-grab.
vlock

This is now a bourne shell script parsing the command line options (all the other programs take no options) and calling the other programs in the right order. For security reasons all paths absolute. Calling vlock with the following command line:

vlock --disable-sysrq --all

will call the programs in the following fashion:

/usr/local/sbin/vlock-nosysrq /usr/local/sbin/vlock-grab /usr/local/sbin/vlock-auth

in the default installation.

Right now the now tools lack documentation other than what is written here. This is also on purpose as I'm not quite sure that the interface is final. There will probably be another blog post soon about general security considerations. As always, I'm looking forward to feedback via email: frank-vlock@benkstein.net.


vlock now on freshmeat

vlock now has a freshmeat project page. Please go ahead and rate if you like vlock.


Crabadonk-Update

crabadonk ist mal wieder so weit, dass der offizielle SILC-Server das erste Paket meines Clients versteht und antwortet. Der Client wiederum versteht zwar die Antwort, kann damit aber noch nichts anfangen:

running ['garbledina', '-c', 'silc.c3d2.de'] in test environment
2007/07/17 10:49 +0200 [-] Log opened.
2007/07/17 10:49 +0200 [-] Starting factory <garbledina.client.connect.SILCClientFactory instance at 0xae28c3cc>
2007/07/17 10:49 +0200 [Uninitialized] Sending a packet: SILCPacket(source=NoID(), destination=NoID(), payload=KeyExchangeStart(version=u'SILC-1.2-0.0 garbledina', key_exchange_groups=(u'diffie-hellman-group1',), pkcs_algorithms=(u'rsa',), encryption_algorithms=(u'aes-256-cbc',), hash_algorithms=(u'sha1',), hmac_algorithms=(u'hmac-sha1-96',), compression_algorithms=(u'none',), flags=0, cookie='\xb9!d\xa3>FU\xfb\x8aS\xa6n\xfc\xfe\xec\x13'), flags=0)
2007/07/17 10:49 +0200 [Uninitialized] Sending a raw packet: RawSILCPacket(source_type=0, source_data='', destination_type=0, destination_data='', payload_type=13, payload_data='\x00\x00\x00p\xb9!d\xa3>FU\xfb\x8aS\xa6n\xfc\xfe\xec\x13\x00\x17SILC-1.2-0.0 garbledina\x00\x15diffie-hellman-group1\x00\x03rsa\x00\x0baes-256-cbc\x00\x04sha1\x00\x0chmac-sha1-96\x00\x04none', flags=0)
2007/07/17 10:49 +0200 [SILCClientProtocol,client] Received a raw packet: RawSILCPacket(source_type=1, source_data='Q\xa9\x89-\xc2\x02\x1a\xb9', destination_type=0, destination_data='', payload_type=13, payload_data='\x00\x04\x00o\xb9!d\xa3>FU\xfb\x8aS\xa6n\xfc\xfe\xec\x13\x00\x1aSILC-1.2-1.0.3 silc-server\x00\x15diffie-hellman-group1\x00\x03rsa\x00\x0baes-256-cbc\x00\x04sha1\x00\x0chmac-sha1-96\x00\x00', flags=0)
2007/07/17 10:49 +0200 [SILCClientProtocol,client] Unhandled Error
        Traceback (most recent call last):
          File "/usr/lib/python2.4/site-packages/twisted/python/log.py", line 48, in callWithLogger
            return callWithContext({"system": lp}, func, *args, **kw)
          File "/usr/lib/python2.4/site-packages/twisted/python/log.py", line 33, in callWithContext
            return context.call({ILogContext: newCtx}, func, *args, **kw)
          File "/usr/lib/python2.4/site-packages/twisted/python/context.py", line 59, in callWithContext
            return self.currentContext().callWithContext(ctx, func, *args, **kw)
          File "/usr/lib/python2.4/site-packages/twisted/python/context.py", line 37, in callWithContext
            return func(*args,**kw)
        --- <exception caught here> ---
          File "/usr/lib/python2.4/site-packages/twisted/internet/selectreactor.py", line 139, in _doReadOrWrite
            why = getattr(selectable, method)()
          File "/usr/lib/python2.4/site-packages/twisted/internet/tcp.py", line 362, in doRead
            return self.protocol.dataReceived(data)
          File "/usr/lib/python2.4/site-packages/twisted/protocols/stateful.py", line 40, in dataReceived
            next = state[0](d)
          File "/home/frank/Projects/crabadonk/_test_prefix/lib/garbledina/protocol/raw_packet.py", line 202, in packetRestReceived
            flags = self.packet_flags))
          File "/home/frank/Projects/crabadonk/_test_prefix/lib/garbledina/protocol/packet.py", line 54, in packetDataReceived
            payload = payload_types[rawPacket.payload_type].disassemble(
          File "/home/frank/Projects/crabadonk/_test_prefix/lib/garbledina/protocol/packet.py", line 16, in packetReceived
            raise NotImplementedError
        exceptions.NotImplementedError:

2007/07/17 10:49 +0200 [SILCClientProtocol,client]
2007/07/17 10:49 +0200 [SILCClientProtocol,client] Stopping factory <garbledina.client.connect.SILCClientFactory instance at 0xae28c3cc>
2007/07/17 10:49 +0200 [-] Main loop terminated.

Ich werde das für die nächsten Tage erstmal damit belassen, da ich eigentlich anderes zu tun habe und mich mit garbledina nur davon ablenke. Wer will, darf gerne mit mir schimpfen.


Crabadonk!

Wie die meisten sicherlich wissen, habe ich schon vor längerer Zeit mit einer SILC-Implementation in Python unter dem Namen "garbledina" begonnen. Trotz kleinerer Erfolge dümpelte das Projekt leider nur sehr langsam vor sich hin. Zwischenzeitlich lief zwar ein minimaler Server, bei dem es sogar möglich war, Channels zu joinen und sich selbst Nachrichten zu schreiben. Aktuell ist der Code nach mehreren Refaktorisierungen aber so kaputt, dass gar nichts mehr geht.

Ich habe zwischenzeitlich schon überlegt, das Projekt ganz aufzugeben. Zum einen verdirbt einem die Standard-Implementation von SILC oft den Spaß, indem sie auf das Kippen weniger Bits dankbar mit einem Segmentation-Fault reagiert oder sich einfach stumm stellt. Zum anderen bereitet das SILC-Protokoll selbst oft Kopfschmerzen, da die Spezifikation an einigen entscheidenden Stellen Lücken aufweist (die man dann durch Lesen in vorgenannter Implementation auffüllen darf) und an anderen unheimlich komplex ist (Stichwort: Command-Payload).

Wahrscheinlich ist es aber doch zu schade, so viel schon getätigte Arbeit einfach wegzuwerfen. Deshalb will ich jetzt, auf dem vorherigen aufbauend, noch einmal von vorne anfangen. Der neue Branch mit dem Namen "crabadonk" wird mit einem leeren Repository starten und sich zunächst auf die Entwicklung eines Clients/Bots konzentrieren. Ich will mich dabei so weit wie möglich an Twisted Conch orientieren, einer SSH-Implementation in Python. Vermutlich hat sich der Autor von SILC auch von SSH inspirieren lassen, denn beide Protokolle sind in Grundsätzen ähnlich. Wenn mir nichts besseres einfällt, werde ich wahrscheinlich einige Teile in C schreiben (müssen) und dabei auf libtom zurückgreifen.

Nun, dieser Blog-Post soll nicht nur über den aktuellen Status von garbledina/crabadonk informieren, sondern auch zum mitmachen anregen, also:

cg-clone http://cthulhu.c3d2.de/~toidinamai/git/crabadonk.git

oder

git-clone http://cthulhu.c3d2.de/~toidinamai/git/crabadonk.git

Und dann:

cd crabadonk
./bin/testdrive garbledina -c silc.c3d2.de

Aktuell passiert dabei leider noch nichts. Ich hoffe, dass sich das in den nächsten Tagen ändert. Über Anregungen und Kommentare an mailto:frank-garbledina@benkstein.net oder xmpp:frank@benkstein.net freue ich mich sehr. Es darf auch sehr, sehr gerne mitgemacht werden.


toidinamai hat ein neues Blog

Nachdem ich mit pyblosxom nicht mehr so richtig zufrieden war und ich mir sowieso einmal Web-Programmierung anschauen wollte, habe ich mich entschlossen, auch ein eigenes Blog zu schreiben, wie es alle coolen Hacker machen.

Jetzt gibt es also ein toidinamaiblog, zu finden unter http://blog.toidinamai.de/, programmiert mit dem besten Web-Framework der Welt: Divmod Nevow. Beim Design habe ich mich stark von pyblosxom inspirieren lassen: Query-Parameter sind eklig und aktuell finden die in toidinamaiblog noch überhaupt gar keine Verwendung. Alles sind Pfade, REST-like, würde ich sagen. Die Inhalte sind als Text-Dateien organisiert, allerdings mit dem Unterschied, dass grundsätzlich kein Unterschied zwischen Kategorien und Einträgen besteht: Alles sind Verzeichnisse, Einträge enthalten zusätzlich eben noch eine Datei "content" (in meinem Lieblings-Markup reStructuredText natürlich). Zusätzlich hat (fast) jede Seite einen eigenen Atom-Feed: einfach /atom hinten an die URL anhängen. Mit /template kann man übrigens das Template der aktuellen Seite anschauen. Irgendwann demnächst werde ich wahrscheinlich auch Kommentare implementieren und, wenn ich ganz viel Zeit habe, vielleicht auch diesen ganzen Trackback-Unsinn.

Da coole Hacker keine Links kaputt machen, werde ich mich bemühen, dass alles, was auf http://cthulhu.c3d2.de/~toidinamai/blog.py verlinkt, weiterhin funktioniert.

Achso: Quelltext gibt's irgendwann in http://cthulhu.c3d2.de/~toidinamai/git, aber dazu muss ich das Repository erstmal ein bisschen säubern, aktuell sind noch Code und Daten zusammen.


vlock 1.4 released

I just released vlock 1.4, the linux virtual console locker. There were no changes between vlock-1.4 and vlock-1.4-rc2. I took over maintenance over vlock about two weeks ago. I tried to find the original author, Michael K. Johnson, to send him some patches. Instead he suggested that I continue maintaining it myself because he hadn't looked at vlock for a long time himself and was quite suprised that people are still using it. I personally find it the best and most secure locking tools for single user machines because it really locks a machine even when I am logged in some virtual consoles and forgot to log myself out.

The changes from vlock 1.3 include merely my original patch, making it possible to disable the linux sysrq mechanism while all consoles are locked, and some cleanups mostly license clarifications (GPLv2) and fixing some compile time warnings.

The next version will probably no longer contain support for shadow passwords and only rely on PAM for authentification. I'll also probably add two new features:

  • Making it possible to let vlock automatically switch to a new console when locking all consoles and switching back on unlock, probably with support to make this securely possible from X.
  • Making it possible to run an additional command after the display is securely locked. This would make it possible to really make sure the a laptop is secured before going into standby. As vlock is normally installed as a setuid-root program there are some security issues I have to work out first so this will probably not be in the next version.

If you have suggestions, bug reports or patches, or if you use vlock on *BSD please drop me a line at frank-vlock@benkstein.net.


vlock und sysrq

Um meinen Laptop zu sperren, benutze ich am liebsten vlock. Ich habe mir ein kleines Skript geschrieben, das, wenn ich lockscreen in einem Terminal eingebe oder Fn+F2 drücke, auf eine leere Konsole umschaltet und dann dort vlock -a startet. Das sorgt dafür, dass der Rechner damit vollkommen unbenutzbar wird, bis man entweder mein Passwort oder das von root (das es nicht gibt) eingibt. X-Locker mag ich nicht, da man bei denen oft mittels Strg+Alt+Backspace das ganze X beenden kann oder einfach auf eine andere Konsole wechseln und dort entweder eine vergessene Login-Session finden oder anderen Unsinn anstellen.

Das einzige, was mich in an vlock immer gestört hat, war, dass man es ganz einfach austricksen konnte, wenn im Kernel sysrq aktiviert ist. Dann kann man mit AltGr+SysRQ+K (oder anderen Kombinationen, die weniger nett sind), das Programm einfach beenden. Eine Abhilfe ist, sysrq einfach zu deaktivieren. Das sperrt mich aber auch aus dem Rechner aus, wenn X sich durch doofe ATI-Treiber und/oder das Verwenden eines zu neuen Entwickler-Kernels sich mal wieder aufgehängt hat.

Wie gesagt, ein Problem, was mich schon länger gestört hat. Deshalb habe ich es jetzt behoben, Quellen gibt es hier:

http://cthulhu.c3d2.de/~toidinamai/git/vlock.git


ssh_config

ssh(1) verarbeitet /etc/ssh/ssh_config und ~/.ssh/config in einer Art und Weise, dass einmal gemachte Änderungen an der Standardeinstellung nicht ein zweites Mal überschrieben werden können. Das bedeutet, dass die allgemeinsten Einträge unten stehen müssen. Dieses kontraintuitive Verhalten hat mir schon mehrfach Verwirrung bereitet.

Ganz unten in meiner ~/.ssh/config steht nun:

Host *
  ControlPath ~/.ssh/sockets/%r@%h:%p
  ControlMaster auto
  ServerAliveInterval 15
  ServerAliveCountMax 4
  TCPKeepAlive no
  IdentitiesOnly yes
  HashKnownHosts no

ControlPath ~/.ssh/sockets/%r@%h:%p, ControlMaster auto

Diese beiden Anweisungen sorgen dafür, dass man sich zu einem Server mit dem gleichen Account immer nur einmal verbindet. Alle weiteren Verbindungen werden über die erste geführt, über einen Unix-Domain-Socket im Verzeichnis ~/.ssh/sockets. Wenn ich mit scp(1) oder rsync(1) auf einem Server arbeite, auf dem ich mich nur mit Passwort einloggen kann, mache ich einfach in einem anderen Terminal eine Shell auf und werde dann zwischendurch nicht mit weiteren Passwortabfragen belästigt.

Einzig ärgerlich ist, dass sich ssh(1) durch tote Sockets irritieren lässt, sodass man diese per Hand löschen muss, sollte sich ein SSH-Prozess nicht sauber beendet haben.

ServerAliveInterval 15, ServerAliveCountMax 4, TCPKeepAlive no

Diese drei Anweisungen haben sich für mich als hilfreich herausgestellt, damit sich Verbindungen, die nicht mehr reagieren (z.B. durch Wiedereinwahl bei DSL) automatisch beenden. Das Warten auf den TCP-Timeout kann manchmal sehr lange dauern.

IdentitiesOnly yes

Diese Anweisung sorgt dafür, dass, wenn ich den SSH-Agent verwende, nur die Identitäten versucht werden, die ich in der ~/.ssh/config konfiguriert habe. Andernfalls probiert ssh(1) alle vorhandenen Keys durch, wobei die Server nach ungefähr den ersten drei die Verbindung trennen. Ich benutze aber für jeden Host (es sei denn auf zwei Hosts ist das gleiche Home-Verzeichnis gemountet) einen anderen PublicKey, sodass der SSH-Agent ohne diese Anweisung für mich unbenutzbar ist.

HashKnownHosts no

Diese Anweisung verhindert, dass die Hostnamen oder IP-Adressen der Rechner nur als Hash in meiner ~/.ssh/known_hosts gespeichert werden. Dieses Verhalten soll die Verbreitung von SSH-Würmern einschränken und soll IIRC irgendwann zum Standard werden. Da ich meine ~/.ssh/known_hosts aber auch oft von Hand bearbeite, verzichte ich lieber darauf.

Singletons

Eine Sache, die man als Programmierer objektorientierter Sprachen immer wieder braucht, sind Singleton-Objekte. Singletons sind Objekte, die die einzigen Instanzen ihrer spezifischen Klassen sind, es gibt alle möglichen Gründe, warum man so etwas manchmal haben möchte, ein Beispiel aus einer Programmieren-Übung für Java war ein Primzahlen-Generator mittels des Siebs des Erastothenes. Da man die bereits berechneten Primzahlen nicht jedes Mal wieder neu berechnen will, reicht es, wenn man immer wieder das gleiche Objekt zurück liefert, sofern man sicher stellt, dass die Liste der Primzahlen nicht von außen modifiziert werden kann.

In Python spielen zwei Methoden beim Instanziieren einer Klasse eine Rolle. Die Klasse hier soll A heißen, x und y seien beliebige lokale Variablen:

>>> a = A(x, y)

Bei diesem Aufruf wird zunächst die Methode __new__ als Klassenmethode von A aufgerufen (d.h. hier mit den Argumenten A, x und y). Ist der Rückgabewert von __new__ eine Instanz von A (auch Instanzen von Subklassen zählen), wird auf dieser neuen Instanz die Methode __init__ aufgerufen, mit den gleichen Argumenten, wie sie beim Aufruf der Klasse übergeben wurden (und natürlich dem explizitem self).

Eine Möglichkeit also, Singletons zu implementieren, wäre einfach __new__ so zu verändern, dass es immer das gleiche Objekt zurückliefert:

class Singleton(object):
  __instance = None

  def __new__(cls, *args, **kwds):
    if cls.__instance is None:
      cls.__instance = super(Singleton, cls).__new__(cls, *args, **kwds)

    return cls.__instance

Diese Implementation hat mehrere Schwächen. Die meiner Meinung nach größte ist, dass immer wieder __init__ für das Singleton-Objekt aufgerufen wird, da dies ja das einzige Kriterium dafür erfüllt, nämlich Instanz von cls zu sein. (Vorausgesetzt der super-Aufruf liefert ein entsprechendes Objekt.) Dies ist z.B. ein Problem für den Primzahl-Generator, der z.B. in __init__ eine leere Liste erzeugt, in der die berechneten Zahlen gespeichert werden sollen. Die anderen Schwächen, wie z.B., dass in einer Subklasse eine schon existierende Instanz der Superklasse zurückgeliefert wird, ließen sich durch ein bisschen Aufwand leicht beheben.

Mein Ansatz ist aber ein anderer und involviert - wer hätte es gedacht? - Metaklassen. Konstruktoraufrufe in Python sehen nämlich nicht nur wie Funktionsaufrufe aus, sie verhalten sich auch genau so. Für jedes x als Instanz von Y wird der Ausdruck x(...) (dabei steht ... für beliebige Argumente) in den folgenden übersetzt: Y.__call__(x, ...). Für Funktionen führt __call__ in der Funktionen-Klasse function (über den __builtin__-Namespace leider nicht erreichbar, wohl aber als types.FunctionType), eben die Funktion aus und für Klassen (Instanzen von type) implementiert __call__ eben genau das oben beschriebene Verhalten. In Python formuliert, liest sich das also folgendermaßen:

# in der Klassendefinition von type
def __call__(self, *args, **kwds):
  new_object = self.__new__(self, *args, **kwds)
  if isinstance(new_object, self):
    new_object.__init__(*args, **kwds)
  return new_object

Zu beachten ist hier, dass self das Klassenobjekt ist. Der besondere Aufruf von __new__ (mit explitem self) kommt dadurch zustande, dass __new__ bei der Klassendefinion eine Sonderbehandlung erfährt und immer in eine statische Methode umgewandelt wird, damit man dies nicht explizit machen muss. Eine Idee also, um Singletons zu implementieren ist die Verwendung einer Singleton-Metaklasse, die __call__ überschreibt:

class SingletonClass(type):
  singletons = {}

  def __call__(self, *args, **kwds):
    try:
      return self.singletons[self]
    except KeyError:
      self.singletons[self] = super(SingletonClass, self).__call__(*args, **kwds)
      return self.singletons[self]

class Singleton(object):
  __metaclass__ = SingletonClass

Eine Klasse die nun von Singleton erbt, hat automatisch Singleton-Charakteristik:

>>> class A(Singleton):
...   pass
>>> A() is A()
True
>>> class B(A):
...   pass
>>> B()
<__main__.B object at 0xb7c01a8c>
>>> Singleton.singletons
{<class '__main__.B'>: <__main__.B object at 0xb7c01a8c>,
 <class '__main__.A'>: <__main__.A object at 0xb7c01a4c>}

garbledina.util.dispatch

Als dynamisch typisierte Sprache, in der man keine Typen deklarieren muss (oder kann), hat Python keine Methoden- und Funktionsüberladung. Da es aber Programmierprobleme, die sich damit vielleicht eleganter lösen lassen als mit Introspektion (und/oder Duck-Typing), und da ein Typencheck in einer else-if-Kette alles andere als elegant ist, gibt es verschiedene Versuche, Typendeklarationen (optional) in die Sprache einzubauen. Python 3000 wird z.B. sog. Annotations ermöglichen, mit denen man die Argumente und den Rückgabewert einer Funktion mit beliebigen Objekten "dekorieren" kann. Aussehen wird das so:

def f(a : int, b : float) -> str:
  ...

Die Annotations sind dann als Dictionary unter f.__annotations__ verfügbar. Es ist dann die Aufgabe eines Decorators oder ähnlichem, daraus etwas sinnvolles zu machen. Eine Möglichkeit wäre z.B. das Überladen von Funktionen und Methoden zu ermöglichen.

Da mir die existierenden Implementationen von Multimethoden nicht gefallen haben (oder ich sie bevor ich mit dem Programmieren angefangen habe noch nicht kannte), habe ich mir zwei eigene Module dazu geschrieben.

Das erste, garbledina.util.annotate, enthält nur einen Decorator (annotate), der das obige Beispiel in Python 2.4 aufwärts folgendermaßen aussehen lässt:

@annotate(a=int, b=float, **{"return" : str})
def f(a, b):
  ...

Für return könnte man sich noch etwas besseres einfallen lassen, aber dazu hatte ich bisher noch keinen Grund, weil ich diese Funktionalität nicht benötige.

Nun aber zum Titel des Eintrages und dem, worüber ich eigentlich schreiben wollte: In garbledina.util.dispatch existiert ein Decorator, multifunction, der es möglich macht, zusammen mit annotate mehrere Implementationen einer Funktion unter einem Namen zur Verfügung zu stellen. Ich habe mir dabei Mühe gegeben, die Verwendung möglichst bequem zu machen, damit der resultierende Code möglichst natürlich und lesbar aussieht:

@multimethod
@annotate(a=int)
def f(a):
  print "a is an int:", a

@multimethod
@annotate(a=float)
def f(a):
  print "a is an float:", a

Zu beachten ist, dass in Python Decorator in umgekehrter Reihenfolge ausgeführt werden. Dies mag zwar zunächst ungewöhnlich erscheinen, macht aber gerade das Beispiel lesbarer und ist auch natürlich, wenn man bedenkt, dass die Funktionsdefinition zuerst ausgeführt wird. Das Beispiel definiert also zweimal die Funktion f, einmal für Ganzzahlen und einmal für Gleitpunktzahlen. Dabei werden auch Subklassen erkannt, d.h. eine Implementation für object würde alle verbliebenen Fälle fangen. Dies funktioniert auch für Funktionen mit verschieden vielen Argumenten und Default-Werten, wobei hier die kürzeste Übereinstimmung genommen wird. Funktionen mit variablen Argumentlisten (*args) sind nicht unterstützt und beliebige Keywords (**kwds) werden einfach ignoriert. Ich weiß noch nicht, ob es sich lohnt, diese Fälle zu implementieren.

Das die beiden Funktionen zusammen gehören, wird global am Namen erkannt, d.h. dass die verschiedenen Implementationen über verschiedene Module verteilt sein können (obwohl ich mir nicht sicher bin, ob das "pythonic" ist). Es ist auch möglich eine Implementation explitzit an einer Multifunktion zu registrieren.

Worauf ich in der aktuellen Version besonders stolz bin, ist, dass in der aktuellen Version auch Methoden richtig funktionieren. Methoden und Funktionen sind ja in Python sehr eng verwandt (dazu vielleicht ein späterer Artikel) und das habe ich versucht auch in den Multimethods nachzubilden. Hinzu kommt, dass auch das annotieren des ersten Parameters (self) richtig funktioniert, was etwas schwierig war, da das Klassenobjekt der Klasse, in der die Methoe definiert wird, zur Definitionszeit der Methode noch nicht existiert und deshalb nicht an annotate übergeben werden kann. Eigentlich will man dies auch gar nicht, denn der Typ von self ist ja schon durch den Kontext der Methodendefinition klar. Genau diesen Kontext benutze ich mittels der Verwendung von schwarzer Bytecode-Magie und ... na? ... Metaklassen. :-)

Dies kann dann also folgendermaßen aussehen:

class Test(object):
    @multimethod
    @annotate(a = int)
    def test(self, a):
        print "A"

    @multimethod
    @annotate(a = float)
    def test(self, a):
        print "B"

class Test2(Test):
    @multimethod
    @annotate(a = int)
    def test(self, a):
        print "C"

Verwendung:

t = Test()

t.test(1) # -> A
t.test(1.0) # -> B

t2 = Test2()

t2.test(1) # -> C
t2.test(1.0) # -> B

Gerade den letzten Aufruf richtig hinzubekommen, hat mir etwas Kopfzerbrechen bereitet und ich muss noch versuchen, den Code dazu noch klarer zu strukturieren, weil ich sonst bald nicht mehr verstehe, warum das funktioniert. Jedenfalls bin ich stolz genug darauf, dass ich überlege dispatch.py als separates Paket zur Verfügung zu stellen und zur Inklusion in Python anzubieten. (Ein bisschen Motivation würde helfen. :-)


X is not type and isinstance(X, X)

In einem früheren Blog-Artikel über Metaklassen habe ich behauptet, dass in Python die eingebaute Klasse type die einzige Klasse ist, die ihre eigene Instanz ist. Dies möchte ich jetzt korrigieren: Obwohl es wahrscheinlich stimmt, ist es nicht unmöglich Klassen zu erzeugen, die ihre eigene Instanz sind:

class T(type):
  pass

class TT(type):
  __metaclass__ = T

TT.__class__ = TT

Zugegeben, dies ist nicht der Standard-Weg, um ein Objekt einer Klasse zu erzeugen, und mir fällt jetzt aus dem Stehgreif auch nicht ein, wozu man dieses Verhalten benutzen kann (eventuell für Singletons?), aber es ist möglich.

Die erste Klasse braucht man übrigens nur, weil Python nur erlaubt, die Klasse von Objekten zu ändern, wenn sie Instanz einer Klasse sind, die in Python geschrieben ist.


.pythonrc

In Python 3000 sind nun endlich print und exec Funktionen. Dies bedeutet aber, dass meine .pythonrc nicht mehr ohne Änderungen auf beiden Versionen funktioniert. Da ich in meiner aktuellen .pythonrc auch Features verwende, die vor 2.5 nicht verfügbar sind, habe ich das jetzt etwas modularer gestaltet: die .pythonrc schaut nur noch nach der ausgeführten Python-Version und führt dann .pythonrcx.y.z aus, wobei x, y und z für die Teile der Versionsnummer stehen. .y und .z dürfen weggelassen werden:

def getpythonrc():
    import sys
    import os.path

    l = len(sys.version_info)
    for i in xrange(l):
        version = ".".join([str(x) for x in sys.version_info[:l-i]])
        pythonrc_name = os.path.expanduser("~/.pythonrc") + version
        try:
            pythonrc = open(pythonrc_name)
        except:
            pass
        else:
            print("Using " + repr(pythonrc_name) + " as startup file")
            return pythonrc

pythonrc = getpythonrc()
if pythonrc is not None:
    exec(pythonrc)

del pythonrc, getpythonrc

try:
    del __file__
except:
    pass

Diese Version läuft bei mir jetzt von Python 2.1 bis Python 3000. Eventuell möchte man, dass alle gefundenen Dateien ausgeführt werden (also für Python 2.5 .pythonrc2.5.0, .pythonrc2.5 und .pythonrc2 - am besten noch in umgekehrter Reihenfolge), das habe ich mir noch nicht genau überlegt.


Closures

Eine Sache, die mich beim Programmieren der Metaklassen in garbledina mehrmals gestört hat, ist, dass Python ein sehr seltsames Verhalten zeigt, wenn man Funktionen in for-Schleifen definiert. Abhängig davon, wie sehr ich darüber nachdenke, verstehe ich manchmal, warum das so ist, und manchmal nicht:

def f():
    functions = []
    for i in xrange(10):
        def g():
            return i**2
        functions.append(g)
    return functions

functions = f()

print map(apply, functions)
# [81, 81, 81, 81, 81, 81, 81, 81, 81, 81]

Eigentlich hätte ich hier gerne [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] als Ausgabe gehabt. Das Problem ist, dass Python nur ein Closure baut (in Python repräsentiert durch cell-Objekte):

for g in functions:
    print g.func_closure

Das Problem hat mich jedenfalls dazu angeregt, mich etwas näher mit Python-Bytecode zu beschäftigen. Wer mehr dazu wissen möchte, der schaue in meinen del.icio.us-Feed. Herausgekommen ist ein Decorator, der für Python-Funktionen die Closure-Variablen verändern kann. Zusätzlich kann er auch globale Variablen als Closures umbinden. Obiges Beispiel sieht dann also folgendermaßen aus:

def f():
    functions = []
    for i in xrange(10):
        @closure(i=i)
        def g():
            return i**2
        functions.append(g)
    return functions

functions = f()

print map(apply, functions)
# [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

Wie gesagt, auch folgendes funktioniert:

@closure(a="Hello, world")
def g():
  print a

Und da nur eine Referenz und gespeichert wird, hat das auch folgende lustige Effekte:

@closure(a=[])
def g():
  a.append(len(a))
  print a

g() # -> [0]
g() # -> [0, 1]
g() # -> [0, 1, 2]

a verhält sich hier für g quasi wie eine statische Variable in C, neu binden ist aber nicht möglich. Vielleicht implementiere ich das noch. Dann aber wahrscheinlich unter einem anderen Namen, da die Funktionalität dann doch stark verschieden wäre.

Der Code dazu wird sich irgendwann in garbledina wiederfinden. Wahrscheinlich irgendwo unter garbledina.util.

Übrigens würde mich sehr interessieren, wie sich das erste Beispiel bei anderen Programmiersprachen, bei denen Funktionen First-Class-Objekte sind, z.B. LISP oder Dylan, verhält. (Kommentare gerne per Jabber an mich.)


Frame-Objekte

Frame-Objekte in Python entsprechen Zuständen der virtuellen Maschine in Python. Sie beinhalten z.B. Informationen über den aktuell ausgeführten Code (als Code-Objekt), dessen lokale und globale Variablen als Dictionaries und - vielleicht am wichtigsten - einen Referenz auf den vorhergehenden Frame. Normalerweise werden Frame-Objekte in Python nur zum Erzeugen von Tracebacks beim Exception-Handling gebraucht. Es gibt aber daneben auch eine Funktion im Modul sys, die das dem aktuellen Frame entsprechende Objekt zurückliefert: _getframe. Der Name, beginnend mit einem Unterstrich, deutet es schon an und die Dokumentation macht es ausdrücklich:

This function should be used for internal and specialized purposes only.

Man kann damit also eine ganze Menge Unsinn anstellen. Aufgrund Implementation von Funktionen in Python ist es nicht möglich deren lokale Variablen zu manipulieren für viele Zwecke ist aber schon das Lesen ausreichend, um Verwirrung zu stiften:

>>> import sys
>>> def get_x():
...   f = sys._getframe(1)
...   return f.f_locals["x"]
...
>>> def f(x):
...   return get_x()
...
>>> f("Hello")
'Hello'

Das würde es z.B. möglich machen so etwas wie Rubys Blöcke in Python zu implementieren. Aber auch für billige Metaprogrammierung lassen sich Frames missbrauchen, denn bei Klassen sind die lokalen Variablen (d.h. die Attribute der Klasse) veränderbar:

>>> def abc():
...   f = sys._getframe(1)
...   l = f.f_locals
...   l["a"] = 1
...   l["b"] = 2
...   l["c"] = 3
...
>>> class Test(object):
...   abc()
...
>>> T.a, T.b, T.c
(1, 2, 3)

An einer Stelle habe ich _getframe auch schon einmal benutzt, um in einer Funktion eine Rekursion, die durch Verhalten in fremden Code auftauchen konnte. Sich andere lustige oder nützliche Verwendungen einfallen zu lassen, sei dem geneigten Leser zur Übung überlassen.


Subklassen-Iterator

Kleine Spielerei:

def iterclasses(base):
    """
    Return an iterator over all direct and indirect subclasses
    of the given base class.
    """
    yield base

    subclasses = type(base).__subclasses__(base)

    for klass in subclasses:
        for subclass in iterclasses(klass):
            yield subclass

Metaklassen II

Nun, da ich die Theorie hinter Metaklassen etwas beleuchtet habe. Möchte ich ein bisschen zeigen, wofür man Metaklassen verwenden kann und warum und wofür ich sie verwende. Erst einmal ein kleines Beispiel, das verdeutlichen soll, wie man Metaklassen verwendet und wie sie sich "anfühlen":

>>> class CoolType(type):
...   def __new__(cls, name, bases, class_dict):
...     print "Creating new class %r with bases %r and dict %r" % (name, bases, class_dict)
...     # oder: super(CoolType, cls).__new__(cls, name, bases, class_dict)
...     return type.__new__(cls, name, bases, class_dict)
...   def hello(self):
...     print "Hello from %r." % (self,)
...
>>> class CoolClass:
...   __metaclass__ = CoolType
...
Creating new class 'CoolClass' with bases () and dict {'__module__': '__main__', '__metaclass__': <class '__main__.CoolType'>}
>>> type(CoolClass)
<class '__main__.CoolType'>
>>> CoolClass.hello()
Hello from <class '__main__.CoolClass'>.

Wie man sieht, wird das neue Klassenobjekt in __new__ erzeugt, dabei könnte man z.B. das Dictionary der Klasse noch verändern, um der Klasse noch andere Eigenschaften, als die im Klassenblock definierten, zu verleihen. In der Metaklasse definierte Methoden werden direkt zu Klassenmethoden, mit einem wichtigen Unterschied:

>>> c = CoolClass()
>>> c.hello()
  File "<stdin>", line 1, in <module>
AttributeError: 'CoolClass' object has no attribute 'hello'

Instanzen der neu erstellten Klassen haben keine direkte Verbindung zur Klasse ihrer Klasse. Dies ist sinnvoll, da die Metaklasse ja die Eigenschaften der Klasse festlegt, nicht die ihrer Objekte.

Wie im letzten Blog-Eintrag beschrieben, erhalten Unterklassen die Metaklasse ihrer Basisklasse:

>>> class OtherCoolClass(CoolClass):
...   pass
...
Creating new class 'OtherCoolClass' with bases (<class '__main__.CoolClass'>,) and dict {'__module__': '__main__'}
>>> OtherCoolClass.hello()
Hello from <class '__main__.OtherCoolClass'>.

Wichtig ist, dass Python erfordert, dass die Metaklasse einer neuen Klasse Basisklasse der Metaklassen aller Basen sein muss:

class OtherCoolType(type):
...   pass
...
>>> class OtherCoolClass:
...   __metaclass__ = OtherCoolType
...
>>> class TooCoolAClass(CoolClass, OtherCoolClass):
...   pass
...
Creating new class 'TooCoolAClass' with bases (<class '__main__.CoolClass'>, <class '__main__.OtherCoolClass'>) and dict {'__module__': '__main__'}
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 4, in __new__
TypeError: Error when calling the metaclass bases
    metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases

Stellt sich dieses Problem einmal bei extensiver Metaklassenbenutzung, hilft hier der Einsatz einer Metaklassenfaktorfunktion, die eine neue Metaklasse als Unterklasse aller Basismetaklassen erstellt. Wer dazu näheres wissen möchte, der möge sich durch den Quelltext von garbledina wühlen.

In garbledina benutze ich Metaklassen an verschiedenen Stellen. Sie sind vor allem hilfreich, wenn man eine große Anzahl von Klassen definieren möchte, deren gemeinsame Eigenschaften sich nicht allein durch die Basisklasse bestimmen lassen. In SILC gibt es z.B. fast 30 verschiedene Payload-Arten und fast 30 verschiedene Client-Befehle. Eine der Metaklassen die ich hierbei verwende ist garbledina.util.container.ContainerClass. Sie ermöglichst es auf einfache Weise Klassen zu definieren, deren Objekte bestimmte Attribute haben. Die Metaklasse generiert zu einem Tupel von Namen den Konstruktor mit optionalen Default-Argumenten und Accessoren, die dafür sorgen, dass ein Attribut nur einmal gesetzt werden kann:

>>> from garbledina.util import container
>>> class Person(container.Container):
...   properties = ("name", "gender", "birth_date")
...
>>> p = Person("fnord", "fnord", "fnord")
>>> p
<Person: name='fnord', gender='fnord', birth_date='fnord'>

Wie man sieht, macht dies den Quelltext um einiges kürzer und dadurch besser wartbar.


Metaklassen I

Seit Python 2.2 gibt es keinen Unterschied mehr zwischen zwischen Typen und Klassen, wenn diese von object abgeleitet sind. Die, die es nicht sind, werden alte Klassen (classic classes) genannt. Ich persönlich verwende nur noch neue Klassen (new style classes), da diese einige Vorteile gegenüber dem alten Modell bieten. In der Standard-Bibliothek und in vielen anderen Modulen werden aber aus Kompatibilitätsgründen, Bequemlichkeit oder Unwissenheit meist die alten benutzt. (Default, wenn nicht object zu den Basisklassen gehört.) Mit den genauen Unterschieden zwischen alten und neuen Klassen wird sich aber wahrscheinlich ein anderer Blog-Eintrag beschäftigen.

Vor Python 2.2 war type nur eine eingebaute Funktion, die den Typ eines Objekts zurückgegeben hat:

>>> type("")
<type 'str'>
>>> type(1)
<type 'int'>
>>> type(1) is int
True

Da der Unterschied zwischen Funktionen und Klassen in Python nicht so groß ist, funktioniert dies immer noch, type ist nun aber auch eine Klasse, und zwar diejenige, von der alle neuen Klassen sind. Da type selber eine neue Klasse ist, ist type die einzige Klasse, die gleichzeitig ihre eigene Instanz ist:

>>> isinstance(type, type)
True

Wem das nicht verwirrend genug ist, der mag sich über Beziehung von object zu type den Kopf zerbrechen, die hier nicht so wichtig ist:

>>> isinstance(type, object)
True
>>> isinstance(object, type)
True
>>> issubclass(type, object)
True
>>> issubclass(object, type)
False

Es gab mal ein Dokument, das auf diesen Aspekt näher einging und das auch mit hübschen Ascii-Graphiken illustrierte. Leider finde ich den Link dazu nicht mehr.

Um ein neues type-Objekt (eine Klasse) zu erzeugen, kann man type mit drei Argumenten aufrufen: dem Namen der neuen Klasse, dem Tupel der Basisklassen und dem dict-Objekt (Dictionary) der Basisklassen:

>>> type("Test", (), {})
<class '__main__.Test'>

object wird automatisch zu den Basisklassen hinzugefügt, wenn es nicht schon implizit (als Basisklasse einer der Basen) oder explizit als Basis aufgelistet ist. Das Definieren einer Klasse mit einem class-Block ist tatsächlich nur eine syntaktisch schönere Form für so einen Aufruf.

Nun, da klar ist, dass das Erzeugen einer neuen Klasse gleich dem Instanziieren von type (bzw. classobj für alte Klassen) ist, ist zu erwarten, dass man Unterklassen von type erzeugen kann und neue Klassen als Instanzen dieser. Dies ist tatsächlich der Fall. Diese Unterklassen von type heißen Metaklassen (Klassen von Klassen, Klassenklassen). Um herauszufinden von welchem Typ eine neue Klasse sein soll, geht Python den folgenden Weg:

  • Gibt es eine Variable __metaclass__ im Klassenkontext, wird diese genommen.
  • Gibt es Basisklasse, die eine neue Klasse ist, wird deren Klasse genommen.
  • Gibt es nur alte Basisklassen wird classobj (alte Klasse) genommen.
  • Wenn es eine globale Variable __metaclass__ gibt, wird diese genommen.
  • Andernfalls wird classobj genommen.

Der letzte Punkt ist etwas schade, wird sich aber aus Kompatibilitätsgründen erst in Python 3000 ändern. Dadurch sind die meisten Klassen, denen man normalerweise begegnet, alte Klassen (s.o.).


IPv6 sinnvoll?

Ich spiele mit dem Gedanken, im ChaosVPN/DIAC24 in Zukunft nur noch IPv4 zu unterstützen und IPv6 allenfalls passiv zu dulden. Ich kann nicht sagen, dass ich es nicht versucht hätte, nur sieht der Fall für IPv6 denkbar schlecht aus. Der Konfigurations- und Verwaltungsaufwand durch IPv6 ist ungleich höher als bei IPv4, dauernd gehen irgendwelche (*hust*) Sachen aus unerfindlichen Gründen nicht und Vorteile bringt es so gut wie keine. Die Leute, die von mir aktuell noch ein Prefix delegiert haben, sollten sich nach einem freien Tunnelbroker umsehen. Die anderen werde ich, falls ihr Link richtig funktioniert, ja weiterhin erreichen können. Die nächste Überlegung wäre, Dienste, die ich früher - eigentlich nur um cool zu sein - IPv6-fähig gemacht habe, wieder auf IPv4 einzuschränken, mal sehen.


Obfuscated Python

Kleine Spielerei:

#!/usr/bin/env python
exec """
eJx1U8Fu2zAMvfsrWBVBZCx11mLooUB226GnDNgxCwwlphMBjiRI9JLs6yfJcmw3qw+WRPO9Rz7R
jw/L1tnlTqolqj9grnTUKpMnoy2Bu7p+q4edISOswyyrsAa9q1u3F4RcKtPSAnRLfs3fMvBPLa2j
RiqEFcTvhUVRhQDPs5gh6yGpcCQsubOkI2ePDyyRhKdjLc5WeqUbIHE4zz4ofRkp9SoT+BwvuAfG
2G81z+8/uwLVXlfI2d9G7lh+O+6Ew9dvLP8PZu7ZigqnebdAxxPVOtNOQiqeuoteWt9B72uxNiS1
+hnjvHXigCs2M1YfYPP0dMTGbGEjlX/5ErYsVRMxbgHCHoIdHWsRlzLERn43qHgI5fAdXgaLEwSt
1ZYz0tqXqa6BsD2hIsd6huCul/DD4S+skmpkxxD25y6d7HXQ6LG1bDDWsPm6XQCzqYu7/FSxDpSC
ekQOq9U09rzNp6D7hmYWhKogLBaBjghOnDBWwmAGQzU94VASXvZoCPj7+kfgWsD6V9x80DTCuQ8D
O2n1ObR6Tq0mzndV4SWSjS8i8XyquwAcpVupCPB2DheAF0n8pR/+T37RLPPWlqXyNpRlsJSVZZjM
smQdezem2T+coDvV
""".decode("base64").decode("zlib")

Quelltext


OpenVPN im Server-Modus

Auf cthulhu läuft ein OpenVPN im Server-Modus. Da auch IPv6 über den Tunnel laufen soll, muss OpenVPN im TAP-Modus laufen. Dabei verhält sich das Tunnel-Gerät wie eine virtuelle Ethernet-Karte. Das bringt mit sich, dass Clients ARP-Anfragen starten müssen, bevor sie mit irgendjemandem auf dem Tunnel reden können. Will man dazu noch, dass die Clients sich gegenseitig sehen, steht man vor einem Problem: ARP-Spoofing ist Tür und Tor geöffnet, keiner kann mehr sicher sein, mit wem er wirklich auf dem Tunnel spricht. Eine Alternative wäre in OpenVPN die Option client-to-client auszuschalten und den Client etwas vermurkste Routen beizubringen. Da sich das etwas schwierig beschreiben lässt, mache ich am besten mal ein Beispiel:

Auf cthulhu liegt auf dem Tunnel das Netz 172.22.110.0/26. cthulhu selbst hat die IP 172.22.110.1. pledge (172.22.110.6), mein Laptop, und zee (172.22.110.3), mein Server, haben jeweils einen Tunnel dorthin. Auf den einzelnen Rechnern sieht es folgendermaßen aus (modulo IP-Adresse und Interface-Namen):

$ ip -o -4 address show dev tap0
21: tap0    inet 172.22.110.1/26 brd 172.22.110.63 scope global tap0
$ ip route show | grep '^172\.22\.110'
172.22.110.0/26 dev tap0  proto kernel  scope link  src 172.22.110.1

Schaltet man nun client-to-client in OpenVPN aus, können pledge und zee nicht mehr miteinander reden, da die ARP-Anfragen nicht mehr durchgelassen werden (was man ja erreichen wollte). Um nun doch wieder Kommunikation zwischen den beiden Rechnern zu erlauben, muss man die Routen etwas umbiegen, so dass diese z.B. folgendermaßen aussehen:

$ ip -o -4 address show dev tap0
21: tap0    inet 172.22.110.6/32 brd 172.22.110.6 scope global tap0
$ ip route show | grep '^172\.22\.110'
172.22.110.1 dev tap0  scope link
172.22.110.0/26 via 172.22.110.1 dev tap0

Diese Änderung muss man natürlich nur auf den Clients vornehmen. Auf dem Server bleibt alles wie gehabt, denn dieser kann ja alle Clients sehen. Eigentlich ist das recht praktikabel: die Clients schicken keine ARP-Requests für andere Clients mehr sondern lassen den Server die Pakete an diese Routen. Allerdings hat man damit zwei neue Probleme: OpenVPN bietet von hause aus keine Möglichkeit, per PUSH den Clients diese Option mitzuteilen. Entweder trägt diese Konfiguration also auf allen Clients statisch in eine up.sh ein, oder man denkt sich ein etwas flexibleres System mit der setenv-safe-Option aus, wofür man allerdings OpenVPN 2.1 braucht. Das zweite Problem bestand bei mir darin, dass Quagga sich weigerte, Routen per BGP zu akzeptieren, wahrscheinlich weil es nicht verstanden hat, dass 172.22.110.1 doch direkt verbunden ist.

Die Lösung, die mir dabei einfiel, war recht einfach: man lässt die Routen und Netzmasken wie sie sind, schaltet aber trotzdem client-to-client aus. Damit sich die Clients gegenseitig erreichen können, genügt es eigentlich, wenn der Server, hier also cthulhu, ARP-Proxy für alle Clients spielt. Das bedeutet, dass er alle ARP-Anfragen der Clients mit seiner eigenen MAC-Adresse beantwortet und die Pakete, die er dann bekommt, einfach an die jeweils anderen Clients weiterroutet. Linux bietet zwar die Möglichkeit zum ARP-Proxying, aber man kann es scheinbar nicht auf einem einzelnen Interface aktivieren. Ist auch verständlich, denn wer erwartet schon, dass auf einem Ethernet-Gerät die anderen Rechner sich nicht gegenseitig nicht sehen können. Es musste also ein Tool her, was diese Aufgabe übernimmt. Was einem dabei als erstes einfällt, sind diese bösen Hacker-Werkzeuge zum ARP-Spoofing. Leider hatten sich die von mir gefunden dafür nicht geeignet, denn sie waren erstens nur dazu ausgelegt, nur eine einzelne IP zu Spoofen (z.B. den Router des LANs), d.h. ich müsste im Extremfall 61 Kopien davon laufen lassen (nicht praktikabel!), und zweitens sind diese bestimmt nicht im Hinblick auf Sicherheit und Zuverlässigkeit programmiert.

Was mir am Ende aber doch über den Weg lief, war ipsentinel. Das ist ein Daemon, der eigentlich dazu gedacht ist, ARP-Anfragen für unbenutzte IPs zu beantworten. Der Programmierer, Enrico Scholz, hat diesen aber flexibel genug gemacht, dass er natürlich auch ARP-Anfragen für benutzte IPs beantworten kann. Dazu noch als gespoofte MAC-Adresse die des Server festlegen, bingo! Schön ist, dass er die Möglichkeit vorgesehen hat, den Daemon als unprivigierten Nutzer in einem chroot-Verzeichnis laufen zu lassen. Ich vermute, er hat sich bei der Programmierung ein bisschen von DJBs Prinzipien inspirieren lassen.

Auf cthulhu läuft also jetzt ip-sentinel parallel zu OpenVPN, natürlich als runit-Dienst. Meine Bedürfnisse hinsichtlich Sicherheit und Wartbarkeit sind dahingehend damit vorerst gestillt. Was mir an der Lösung besonders gefällt, ist, dass die Clients davon nichts merken und dass ich damit sogar die Möglichkeit habe Client-zu-Client-Traffic zu filtern.


toidinamai hat jetzt ein Blog

In Zukunft kann man hier über die Dinge lesen, die mich gerade interessieren und die ich für mitteilenswert halte. Kommentare gibt's nur per Jabber an mich persönlich und die Seite wird als application/xhtml+xml ausgeliefert, geht also nicht in alten Browsern. Falls jemand eine Idee hat, wie man auf der Server-Seite feststellen kann, ob der Client application/xhtml+xml rendern kann, bin ich interresiert.