Die Dokumentation ist zweigeteilt. Dieser Teil enthält eine technische Beschreibung. Die Bedienung und Funktionsweise ist in der Anwenderdokumentation beschrieben.

Aufbau

Die Anwendung besteht im groben aus 4 Komponenten

  • Activity .IodineMain

  • Activity Verbindungseinstellungen .IodinePref

  • Tunnel Service VpnService und den JNI Bindings IodineClient

  • Konfigurationsverwaltung .config.ConfigDatabase und .config.IodineConfiguration

[whiteboard-komponenten] zeigt Architektur der Anwendung:

bilder/Model_model_Architektur.PNG
Abbildung 1. Architektur der Anwendung

Benutzeroberfläche

Die Haupt Activity .IodineMain startet den "VpnService" und steuert ihn über Broadcast Intents. In dieser Activity steuert der Benutzer den Auf- und Abbau der Tunnel. Über ein Button in der ActionToolbar kann eine neue Tunnelkonfiguration angelegt werden.

Die Interaktion zwischen des Benutzers in der Anwendung ist in [whiteboard-gui] visuell dargestellt:

bilder/whiteboard_gui.jpg
Abbildung 2. Graphischer Aufbau der GUI

Konfiguration

Die Tunnelkonfigurationen werden in einer SQLite Datenbank abgelegt. Es existiert mit .config.IodineConfiguration ein leichtgewichtiger Proxy um die Android ContentValues Klasse. Die .config.ConfigDatabase Klasse ist ein SQLiteOpenHelper und kann mehrfach instanziert werden.

VPN-Service

Der VPN Service hat 5 Zustände die er über Broadcast Intents mitteilt. Eine solche Mitteilung wird verschickt wenn sich der Zustand ändert oder dies über ACTION_CONTROL_UPDATE angefordert wurde.

Die Kommunikation der Oberfläche mit dem VPN Service erfolgt mit Broadcasts Intents.

[whiteboard-intents] zeigt die Zustände des Iodine VPN-Service. Rot nummeriert sind die Intents die der Service verschickt um über Statusänderungen zu informieren. Blau nummeriert sind Intents mit denen der Service gesteuert werden kann.

bilder/whiteboard_intents.jpg
Abbildung 3. Status Informations und Steuerungs Intents des VPN Service

JNI

Die JNI Methoden für iodine befinden sich in der Klasse .IodineClient bzw. /jni/iodine-client.c. IodineClient#connect ersetzt dabei prinzipiell die main() des ursprünglichen iodine Client.

Weitere Methoden dienen dem Austausch der vom Server übermittelten Konfiguration und des im System eingestellten DNS Server.

Android VPN-Framework

Seit API Level 14/Android 4 ist es möglich VPN Verbindungen mit Android Anwendungen aufzubauen und zu verwalten.

Die Application benötigt dazu die Permission android.permission.BIND_VPN_SERVICE.

Bevor eine Anwendung das erste mal eine VPN Verbindung aufbauen darf wird Android sicherheitshalber den Benutzer explizit um Erlaubnis fragen.

Dazu wird IodineVpnService.prepare(this) [vpnapi] aufgerufen. Wird null zurückgegeben hat der Benutzer VPN Verbindungen dieser App bereits früher zugestimmt. Andernfalls wird ein Intent zurückgegeben mit dem die Benutzernachfrage initiiert werden kann.

  public void tunnel() {
    Intent intent = IodineVpnService.prepare(this);
    if (intent != null) {
      // Ask for permission
      intent.putExtra(IodineVpnService.EXTRA_CONFIGURATION_ID, configuration.getId());
      startActivityForResult(intent, INTENT_REQUEST_CODE_PREPARE);
    } else {
      // Permission already granted
      startVPNService();
    }
  }

  @Override
  protected void onActivityResult(int requestCode, int resultCode, Intent data) {
    if (requestCode == INTENT_REQUEST_CODE_PREPARE) {
      if (resultCode == RESULT_OK) {
        startVPNService();
      } else {
        // User denied permission
      }
    }
  }

  private void startVPNService() {
    // Start VPN with VPNService.Builder
  }

Der weitere Weg mit dem VPNService.Builder ist geradelinig. Im Fall von iodine wird zunächst der Tunnel über DNS aufgebaut bevor das tun-Interface geöffnet wird.

Nachdem vom Server die IP-Konfiguration mitgeteilt wurde, wird diese im Builder gesetzt und der Tunnel geöffnet:

    // .... IodineVpnService.java :: runTunnel()
    b.addAddress(hostAddress, netbits);
    b.addRoute("0.0.0.0", 0); // Default Route
    b.setMtu(mtu);

    // Opens tun device
    ParcelFileDescriptor parcelFD = b.establish();

    // prevent dns traffic to get through its own tunnel
    protect(IodineClient.getDnsFd());

    // get the filedescriptor
    int tun_fd = parcelFD.detachFd();

    // pass the filedescriptor to iodine
    IodineClient.tunnel(tun_fd);

iodine

Verbindungsaufbau (Handshake)

Der folgende Text zeigt ein Beispiel für den Ablauf eines Handshake. Der genaue Ablauf kann variieren jenachdem wie die Verbindungsparameter gewählt werden.

Hier sind gewählt -m 768 fragment size und ein 9 Zeichen Passwort. Die Gegenstelle ist t.yves.tw. Eine Raw (direkte UDP) Verbindung wurde verhindert indem der Rechner zum Testzeitpunkt keine default Route hatte.

RX/TX aus der Sicht des Servers. Die "*" in den Hostnamen markieren Zeichen die sich aus Random Daten ergeben.

    == Der Client testet die Qualitaet der Uebertragung
    <-- client.c:handshake_qtype_autodetect()
              -> handshake_qtypetest()
              -> send_downenctest()
      hostname[0] = "y"
      hostanme[1] = downenc = 'r'
      hostname[2] = variant = 1 = 'b' (b32)
      hostname[3..5] = rand_seed++
      RX: yrb***.t.yves.tw

    --> 48 bytes aus encoding.h:DOWNCODECHECK
      TX: yrb***.t.yves.tw, 48 bytes data

    == Austausch der Versionsinformationen
    <-- client.c:send_version()
      VERSION=0x00 00 05 02
      hostname[0] = cmd = 'v'
      hostname[1..6] = b32(0,0,5,2,random<<8,random)
      hostname = "vAAAAKAR__"
      RX: vaaaaka****.t.yves.tw

    --> iodined.c:send_version_response()
      der Server bestaetigt mit
      data[0..8] = "VACK" b32(seed>>24, seed>>16, seed>>8, seed, userid)
      TX: vaaaaka***.t.yves.tw, 9 bytes data


    == Senden von Passwort und IP-Konfiguration (Subnetz)
    <-- client.c:send_login()
      cmd = 'l'
      hostname[1..16] = login/password mit seed xored und md5
      hostname[17..18] = seed
      RX: lad24srn4ezmg21qjsfy13msagd0srfq.t.yves.tw

    --> iodined.c:handle_null_request()
      Sendet bei Erfolg die IP Einstellungen wie
      "172.16.0.1-172.16.0.2-1130-16"
      server="172.16.0.1"
      client="172.16.0.2"
      mtu=1130
      netbits=16
      TX: lad24srn4ezmg21qjsfy13msagd0srfq.t.yves.tw
        = 3137322e31362e302e312d3137322e31362e302e322d313133302d3136 (_16)
        = 172.16.0.1-172.16.0.2-1130-16

    == Senden der IP Adresse des Clients
    <-- Request for IP address
      RX: iamin.t.yves.tw

    --> iodined.c:handle_null_request()
        addr = externe IP Adresse des Server (-n Switch)
        reply[0] = 'I';
        reply[1] = (addr >> 24) & 0xFF;
        reply[2] = (addr >> 16) & 0xFF;
        reply[3] = (addr >>  8) & 0xFF;
        reply[4] = (addr >>  0) & 0xFF;
      TX: iamin.t.yves.tw
        = 494e2f737d (_16)

    == Testen auf EDNS Erweiterung
    <-- client.c:handshake_edns0_check()
              -> send_downenctest()
      downenc = 'r' fuer T_NULL 't'
      variant = 1 = 'b' (b32)
      data[0..5]  = "y" downenc variant rand_seed[0..2]
      RX: yrb***.t.yves.tw

    --> iodined.c:handle_null_login() : 937
               -> write_dns( type='R')
      Der Server antwortet mit 48 bytes aus encoding.h:DOWNCODECHECK
      TX: yrb***.t.yves.tw, 48 bytes data


    == Testen der Kodierungen mit verschiedenen Patterns
    <-- client.c:handshake_upenc_autodetect()
      In den folgenden Tests testet der Client ob mit Base128
      kodierte Nachrichten vom DNS Relay korrekt verarbeitet werden.

    --> Der Server schickt die Patterns einfach wieder zurueck.

    == Client legt Kodierung fest, Server bestaetigt
    <-- client.c:handshake_switch_codec()
      hostname[0] = command 's'
      hostname[1] = b32(userid)
      hostname[2] = 'h' (7)
      hostname[3..5] = rand_seed++
      rand_seed++;
      RX: sahmiut.yves.tw

    --> iodined.c:840
      Schreibt den Namen des ausgewaehlten Codecs:
      data="Base128" (kein encoding!)
      TX: sahmiut.yves.tw, 7 bytes of data

    == Anschalten lazy mode (an: Server beantwortet Anfragen nicht sofort)
    <-- client.c:send_lazy_switch()
      hostname[0] = 'o'
      hostname[1] = b32(userid) = 'a'
      hostname[2] = 'l' fuer lazy mode oder 'i'
      hostname[3..5] = rand_seed++
      RX: oalmiv.t.yves.tw

    --> iodined.c:919
      data="Lazy" (kein encoding!)
      TX: oalmiv.t.yves.tw, 4 bytes of data

    ==
    <-- client.c:send_set_downstream_fragsize()
      data[0] = userid;
      data[1] = (fragsize & 0xff00) >> 8;
      data[2] = (fragsize & 0x00ff);
      data[3] = (rand_seed >> 8) & 0xff;
      data[4] = (rand_seed >> 0) & 0xff;
      hostname = 'n' + b32(data)
      RX: naabqbmiw.t.yves.tw

    --> iodined.c:1042
      bestaetigt empfangene Framesize durch Wiederholung

    == Regelmaesige pings fragen den Server nach anstehenden Daten ab
    <-- client.c:send_ping()
      data[0] = userid;
      data[1] = ((inpkt.seqno & 7) << 4) | (inpkt.fragment & 15);
      data[2] = (rand_seed >> 8) & 0xff;
      data[3] = (rand_seed >> 0) & 0xff;
      hostname = 'p' + b32(data)
      RX: paaalcfy.t.yves.tw

    --> iodined.c:1067
      Der Server nutzt die regelmaessigen Pings um Daten an den Client zu liefern.

Der lazy Modus

Wie in der Anwenderdokumentation beschrieben erhöht der Lazy Modus den Durchsatz und senkt die Latenzzeit, wird aber nicht von allen DNS-Relays unterstützt.

Lazy bezieht sich auf das Verhalten des Servers. Der Server wird im Lazy-mode alle Antworten auf Anfragen solange zurückhalten bis er neue Daten für den Client erhalten hat. Im Idealfall also bis das Antwortpaket der getunnelten IP Verbindung angekommen ist.

Diese Verzögerung kann mit manchen DNS-Relays Probleme machen. Der Server kann dies jedoch anhand der Duplikate in den Anfragen erkennen und damit den lazy-mode ausschalten.

Ohne diesen Mechanismus müsste der Client jedoch viel häufiger nach neuen Daten pollen (vgl. HTTP Long polling in Comet oder BOSH).

iodine base(32) Kodierung

Dieses Programm bietet die Base32 Kodierung von iodine für die Kommandozeile zum Debuggen an.

#include <stdio.h>
#include <string.h>

#include "src/base32.h"
#include "src/encoding.h"

int main(int argc, char *argv[]) {
    struct encoder *b32 = get_base32_encoder();
    char buf[512];
    size_t len = 512;

    if (argc != 3) return 0;
    if (*argv[1] == 'd') {
        int r = b32->decode(buf, &len, argv[2], strlen(argv[2]));
        int i;
        printf("Decoded %d bytes:\n", r);
        for (i = 0; i< r; i++) {
            printf("0x%02hhx (%c) ", buf[i], (buf[i] >= '0' && buf[i] <= 'z') ? buf[i] : ' ');
        }
        printf("\n");
    } else if (*argv[1] == 'e') {
        int r = b32->encode(buf, &len, argv[2], strlen(argv[2]));
        printf("Encoded %d bytes in %ld output bytes: >%s<\n", len, r, buf);
    }
    return 0;
}
# gcc test.c src/base32.c -o test
# ./test e abcdefg
Encoded 7 12 bytes: >mfrggzdfmztq<

Änderungen an iodine

Der Code basiert auf der letzten Iodine Version 0.6.0-rc1. Die Änderungen wurden absichtlich möglichst gering gehalten und betragen im wesentlichsten nur ca. 80 Zeilen.

Ein Hauptteil der Änderungen verhindern, dass Android als Linux erkannt wird. Im Gegensatz zu vielen Linux Installationen verwenden Android nicht die GNU libc sondern Bionic libc. Dies ist eine besonders kleine, auf die BSD libc zurückgehende standard C Library. Es fehlen einige Features der glibc wie wide-character support, volle POSIX Thread Unterstützung oder locale Unterstützung. Das Ziel von Bionic ist nicht eine vollständige C Standardbibliothek sondern lediglich eine schlanke Implementierung aller für ein Android nötigen Funktionen.

Im einfachsten Fall scheitert die Ausführung von iodine unter Android an einem system() Aufruf mit dem iodine die IP-Konfiguration anwendet.

Android.mk

Das ursprüngliche iodine Makefile wird nicht verwendet. Es wird das Android NDK Buildsystem verwendet, die Anweisungen dazu liegen in jni/Android.mk. Aus dem Projektverzeichnis kann die Übersetzung der C-Quellen angestossen werden.

org.xapek.andiodine % ~/$NDK_ROOT/ndk-build clean
Clean: iodine-client [armeabi]
Clean: stdc++ [armeabi]
org.xapek.andiodine % ~/$NDK_ROOT/ndk-build
Compile thumb  : iodine-client <= iodine-client.c
Compile thumb  : iodine-client <= tun.c
Compile thumb  : iodine-client <= dns.c
Compile thumb  : iodine-client <= read.c
Compile thumb  : iodine-client <= encoding.c
Compile thumb  : iodine-client <= login.c
Compile thumb  : iodine-client <= base32.c
Compile thumb  : iodine-client <= base64.c
Compile thumb  : iodine-client <= base64u.c
Compile thumb  : iodine-client <= base128.c
Compile thumb  : iodine-client <= md5.c
Compile thumb  : iodine-client <= common.c
Compile thumb  : iodine-client <= client.c
Compile thumb  : iodine-client <= util.c
SharedLibrary  : libiodine-client.so
Install        : libiodine-client.so => libs/armeabi/libiodine-client.so

Die Library wird vom Android SDK automatisch in die APK-Datei eingefügt.

common.c daemon()

Die daemon() Funktion in src/common.c ist gedacht um iodine als Hintergrundprozess laufen zu lassen. Sie ist nur für Linux und BSD vorgesehen.

Das #ifdef erkennt Android als Linux, Bionic unterstützt daemon() jedoch nicht, da die Funktionalität der daemon() Funktion für eine Android App nicht benötigt wird.

Auch in diesem Fall brauchen wir die daemon() Funktion nicht, da iodine in einem von Java gesteuerten Thread laufen wird.

common.c warn()

Die warn() Funktion existiert nicht in der Bionic libc. Die bereitgestellte Implementierung verwendet fprintf auf stderr. Die Meldungen werden in das Android Logging System umgeleitet und sind auch über Logcat nutzbar.

tun.c write_tun() / read_tun()

Wie bei FreeBSD und Windows muss beim schreiben auf das Tun device (write_tun ) kein 4 byte großer Header mit der Adress Family angefügt werden. Entsprechend wird dieser in read_tun() im Fall von Android, FreeBSD und Windows nicht entfernt.

tun.c tun_setip()

Je nach Plattform werden wird die IP-Adresse unterschiedlich gesetzt. Im Fall von Linux geschieht dies mit einem fragwürdigen system("/sbin/ifconfig") Aufruf.

Dies ist unter Android so nicht möglich. Es wurde daher eine globale Datenstruktur tun_config_android (tun.h) angelegt in welcher die zu setzende IP-Adresse, Gegenstelle IP-Adresse und Netzmaske abgelegt wird. Die Inhalte dieser Datenstruktur können von Java über JNI Funktionen abgefragt werden.

Das setzen der IP-Adressen und Routen geschieht über Methoden des Android VPN-Framework in Java.

DNS Headerfiles

Iodine benötigt Konstanten aus arpa/nameser_compat.h und arpa/nameser.h das nicht Teil der Android Libc ist. Die Header wurden als src/dns_android.h hinzugefügt.

Projekt öffnen und bauen

C Quellcodes übersetzen

Um das Projekt zu bauen ist neben dem Android SDK auch das Android NDK erforderlich. Mit dem daraus bereitgestellten Kommando ndk-build werden die C-Quellcodes unterhalb des Verzeichnisses jni/ übersetzt.

andiodine$ $NDK_ROOT/ndk-build clean
Clean: iodine-client [armeabi]
Clean: stdc++ [armeabi]
Clean: iodine-client [x86]
Clean: stdc++ [x86]

andiodine$ $NDK_ROOT/ndk-build
Compile thumb  : iodine-client <= iodine-client.c
.....
SharedLibrary  : libiodine-client.so
Install        : libiodine-client.so => libs/armeabi/libiodine-client.so
Compile x86    : iodine-client <= iodine-client.c
.....
SharedLibrary  : libiodine-client.so
Install        : libiodine-client.so => libs/x86/libiodine-client.so

Entwickeln mit Eclipse

Das Projekt kann über den Importassistenten eingebunden werden:

Import → Android → Existing Android Code Into Workspace

Entwickeln mit Android Studio

  • Choose Import Project, choose project Folder.

  • Select "Create project from existing sources".

Übersetzen mit ant

Using ant

   android project --path .
   ant debug

Die APK liegt unterhalb von bin und kann mit dem ant target install über adb installiert werden.

Anhang