MPPE対応カーネルのコンパイル

pptpdでMS-CHAPv2及びMPPE暗号化を使用するためには、 呼び出されるpppdをMS-CHAPv2+MPPE対応のものに入れ替え、 かつカーネルパッチを当ててMPPE暗号化を有効にしないといけません。

CVSで手に入るpppdの最新ベータではMS-CHAPv2とMPPEが取り込まれているので、 これを使います。 詳しい手順は Nobさんのページ を参照してください。

pppdの2.4.2beta(最新版)のソースをcvsで持ってきておきます。
Nobさんのページ参照。

# apt-get install cvs
Where are your repositories: (削除)
Should the CVS pserver be enabled: No
% cvs -z5 -d :pserver:cvs@pserver.samba.org:/cvsroot co ppp
カーネルソースを持ってきます。自分の使っているカーネルか、より新しいのを。 カーネルの再コンパイルの仕方はあちこちで解説があると思います。
# apt-cache search kernel-source
...
# apt-get install kernel-source-2.4.18
# apt-get install kernel-package
# apt-get install libncurses5-dev
% tar xjf /usr/src/kernel-source-2.4.18.tar.bz2
% cd kernel-source-2.4.18
% make menuconfig
  最初にLoad an Alternate Configuration File(下のほうにあります)で
  使用中のカーネルのconfig (/boot/config-2.4.xx など)
  を読み込みます。
  保存して(.configに書かれます)、終了。
pppに含まれるカーネルパッチを当てて、コンパイル。
% cd ~/ppp/linux/mppe

% sh mppeinstall.sh ~/kernel-source-2.4.18
Is this a 2.2 kernel or 2.4 kernel: 2.4
...
% cd ~/kernel-source-2.4.18
% make menuconfig
  Network DeviceにあるPPP MPPEを有効にします。
% make-kpkg clean
% make-kpkg kernel_image
% cd ..
# mv /lib/modules/2.4.18 /lib/modules/2.4.18.orig …使用中のカーネルが同じ名前の場合
# dpkg -i kernel-image-2.4.18_xx.deb
# reboot

pppdのldap化

pppd自体はpamに対応しているため、pam経由でldapが使えますが、 MS-CHAPv2で用いる認証は生パスワードが必要なためか 必ず/etc/ppp/chap-secretsファイルが参照されます。
pppdのradius認証モジュールを有効にしてradiusサーバにldapの生パスワードを 中継させればいけそうですが、 そのためだけにradiusも面倒なのでソースを書き換えて ldap経由でNTLM認証をするように仕向けます。 slapdはNTLM認証対応に改造しておいてください。

% cd ~/ppp/pppd
% vi chap.c
...
(関数ChapReceiveResponse 86行目あたり)
                /* We do not want to leak info about the chap result. */
                code = CHAP_FAILURE; /* XXX exit value will be "wrong" */
                warn("calling number %q is not authorized", remote_number);
            }
        }

    } else if(ChapMSLdap(explicit_remote? remote_name: rhostname,
                               remmd, (int)remmd_len, cstate)) {
      code= CHAP_SUCCESS;
    } else {
        if (!get_secret(cstate->unit, (explicit_remote? remote_name: rhostname),
                        cstate->chal_name, secret, &secret_len, 1)) {
            warn("No CHAP secret found for authenticating %q", rhostname);
        } else {
% vi chap_ms.c
...
#include <ldap.h>
int
ChapMSLdap(user, remmd, remmd_len, cstate)
     char *user;
     u_char *remmd;
     int remmd_len;
     chap_state *cstate;
{
  const char b64[]=
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
  char pass[6+44+1];
  int i;
  char *p;
  u_char *c;
  char* msg;
  FILE *fp;
  char buf[MAX_NT_PASSWORD];
  char host[64];
  int port;
  char bind[200];
  LDAP *ld;
  char *attr[]= { "userPassword", 0};
  LDAPMessage* lm;
  char *addr= "*";
  struct wordlist *addrs;

  if(cstate->chal_type!=CHAP_MICROSOFT&&cstate->chal_type!=CHAP_MICROSOFT_V2)
    return 0;
  if (remmd_len != MS_CHAP_RESPONSE_LEN) return 0;
  memcpy(pass, "{NTLM}", 6);
  p= pass+6;
  c= remmd+24;
  if(cstate->chal_type==CHAP_MICROSOFT&&!((MS_ChapResponse*)remmd)->UseNT[0])
    c= remmd;
  for(i= 0; i<32; i+=3) {
    int e0, e1, e2;
    if(i==24) {
      if(cstate->chal_type==CHAP_MICROSOFT_V2) {
        ChallengeHash(((MS_Chap2Response*)remmd)->PeerChallenge
        ,cstate->challenge, user, buf);
        c= buf;
      } else {
        c= cstate->challenge;
      };
    };
    e0= c[0];
    e1= i+1<32?c[1]:0;
    e2= i+2<32?c[2]:0;
    *p++= b64[e0>>2];
    *p++= b64[e0<<4&0x30|e1>>4];
    *p++= i+1<32?b64[e1<<2&0x3c|e2>>6]:'=';
    *p++= i+2<32?b64[e2&0x3f]:'=';
    c+= 3;
  };
  *p= 0;
  if(plogin(user, pass, &msg)!=2) return 0;

  /*login complete*/
  port= LDAP_PORT;
  strcpy(host, "127.0.0.1");
  strcpy(bind, "");
  fp= fopen("/etc/pam_ldap.conf", "r");
  if(fp) {
    while(fgets(buf, sizeof(buf), fp)) {
      char *p, *pk, *pv;
      p= buf; while(*p>0&&*p<=' ') p++;
      pk= p; while(!(*p>=0&&*p<=' ')) p++;
      while(*p>0&&*p<=' ') *p++= 0;
      pv= p; while(!(*p>=0&&*p<' '&&*p!='\t')) p++;
      *p= 0;
      if(strcasecmp(pk, "host")==0) {
        strncpy(host, pv, sizeof(host)-1); host[sizeof(host)-1]= 0;
      } else if(strcasecmp(pk, "port")==0) {
        port= atoi(pv);
      } else if(strcasecmp(pk, "bindpppuser")==0) {
        snprintf(bind, sizeof(bind), pv, user);
      } else if(strcasecmp(pk, "base")==0&&bind[0]==0) {
        snprintf(bind, sizeof(bind), "cn=%s,ou=People,%s", user, pv);
      };
    };
    fclose(fp);
  };

  buf[0]= 0;
  if(ld= ldap_init(host, port)) {
    if(ldap_simple_bind_s(ld, bind, pass)==LDAP_SUCCESS &&
    ldap_search_s(ld, bind, LDAP_SCOPE_BASE, 0, attr, 0, &lm)==LDAP_SUCCESS) {
      LDAPMessage* le= ldap_first_entry(ld, lm);
      if(le) {
        char** val= ldap_get_values(ld, le, attr[0]);
        if(val) strncpy(buf, val[0], sizeof(buf)), buf[sizeof(buf)-1]= 0;
      };
      ldap_msgfree(lm);
    };
    ldap_unbind(ld);
  };

  if(buf[0]==0) warn("Cannot get userPassword from LDAP for %s", bind);

  addrs= (struct wordlist*)malloc(sizeof(struct wordlist)+strlen(addr)+1);
  addrs->next= 0;
  addrs->word= (char*)(addrs+1);
  strcpy(addrs->word, addr);
  set_allowed_addrs(cstate->unit, addrs, 0);
  free(addrs);

  if(cstate->chal_type==CHAP_MICROSOFT_V2) {
    GenerateAuthenticatorResponse(buf, strlen(buf)
    , ((MS_Chap2Response*)remmd)->NTResp
    , ((MS_Chap2Response*)remmd)->PeerChallenge
    , cstate->challenge, user, cstate->saresponse);
#ifdef MPPE
    SetMasterKeys(buf, strlen(buf), ((MS_Chap2Response*)remmd)->NTResp
    , MS_CHAP2_AUTHENTICATOR);
    mppe_keys_set = 1;
#endif
  } else {
#ifdef MPPE
    Set_Start_Key(cstate->challenge, buf, strlen(buf));
    mppe_keys_set = 1;
#endif
  };

  return 1; //success
};
...
% vi auth.c
...
/*static*/ int  plogin __P((char *, char *, char **));
/*static*/ void set_allowed_addrs __P((int, struct wordlist *, struct wordlist *));
...
/*static*/ int
plogin(user, passwd, msg)
    char *user;
    char *passwd;
    char **msg;
{
...
static void
plogout()
{
    char *tty;
#ifdef USE_PAM
    int pam_error;

    if (pamh != NULL) {
        pam_error = pam_close_session (pamh, PAM_SILENT);
        pam_end (pamh, pam_error);
        pamh = NULL;
    }
    /* Apparently the pam stuff does closelog(). */
    reopen_log();
#endif
//    char *tty;
    tty = devnam;
    if (strncmp(tty, "/dev/", 5) == 0)
        tty += 5;
    logwtmp(tty, "", "");               /* Wipe out utmp logout entry */
//#endif /* ! USE_PAM */
    logged_in = 0;
}
...
/*static*/ void
set_allowed_addrs(unit, addrs, opts)
    int unit;
    struct wordlist *addrs;
    struct wordlist *opts;
{
...
chap.cの認証でChapMSLdapを呼び出すようにして、 ChapMSLdap内部でplogin経由でpam-ldapにNTLMチャレンジ・レスポンスを渡して 認証させます。 MPPEの暗号化やMS-CHAPv2では、生パスワードが必要となるため、 ログインに成功した場合さらにパスワードを取得します。 この時のldapへの接続方法は/etc/pam_ldap.confの内容に従い、 base が dc=hoehoe,dc=japan の場合は cn=moke,ou=People,dc=hoehoe,dc=japan になります。もし変更したい場合は /etc/pam_ldap.conf に
bindpppuser cn=%s,ou=People,dc=soumu,dc=hoehoe,dc=japan
のように設定してください。

pamを有効にしてコンパイルし、/usr/sbin/pppdを入れ替えます。 別の名前でpptpd専用なpppdを置いてpptpdからそれを指定できればいいんですが、 pptpdが起動するpppdは/usr/sbin/pppd固定なようですので、 apt-get upgradeでpppdが元に戻らないようにholdしておく必要もあります。

% vi pppd/Makefile.linux
...
HAS_SHADOW=y
USE_PAM=y
...
INSTALL= install -o root
LIBS += -lldap
...
# apt-get install libldap2-dev
% cd ..
% ./configure
% make
% cd pppd
% strip pppd
# mv /usr/sbin/pppd /usr/sbin/pppd.orig
# cp pppd /usr/sbin
# chmod ... で元と同じに。
# dselect ...でpppをhold-stateにしておきます。

Windows2000からの接続テスト

# apt-get install pptpd
# vi /etc/ppp/pptpd-options
...
auth
require-mschap-v2
#require-mschap 両方指定してもv2のみ有効です。
require-mppe
mppe-stateful
...
# vi /etc/pam.d/ppp
auth     sufficient pam_ldap.so
account  sufficient pam_ldap.so
session  sufficient pam_ldap.so
auth    required        pam_nologin.so  use_first_pass
auth            required       pam_unix.so      nullok use_first_pass
account         required       pam_unix.so
session         required       pam_unix.so
# vi /etc/ppp/chap-secrets
(最低1行の有効なダミー項目)
# vi /etc/pam_ldap.conf
(必要ならbindpppuserを設定)
/etc/ppp/chap-secretsファイルに最低でも1行の項目がなぜか必要です。
ではWindows2000等からVPNで接続してみましょう。 ユーザ名とパスワードはldapに登録したもの(moke,abcdef)です。 暗号化が有効で接続できましたか?
なお、Windows95等からMSCHAP-v2でログインするにはMicrosoftで公開している パッチが必要です。

アドレス変換

Windows2000等から接続する場合、 ネットワークカードが属しているネットワークレンジ及び、 PPTPサーバ自体のアドレスへのパケットは、 pptp接続中であってもpptp経由せずに直接出力されます。 このため、pptpサーバとpop等のサーバを兼ねる場合、 popサーバとしては内側(/etc/pptpd.confのlocalip)のアドレスを指定する 必要があります。 pptp接続していない時は外側のアドレスを指定しなければならないため、 使い勝手がよくありません。
そこで、外部アドレスだけれども内部からはほとんどアクセスされないアドレスを pptpサーバとしてクライアントマシンに指定させ、 そこへのpptpアクセスをDNATを用いてpptpサーバに割り当てることにします。 こういうアドレスとしてはwww専用サーバのアドレスでもいいですし、 linuxサーバ自身にpppoe接続をさせている場合ではブロードキャストアドレスなどを 用いることができます。
12.34.56.0 : ネットワーク
12.34.56.1 : Linuxボックスのアドレス
12.34.56.7 : ブロードキャスト
の場合、
iptables -t nat -A PREROUTING -d 12.34.56.1 -p tcp --dport 1723 -j DROP
iptables -t nat -A PREROUTING -d 12.34.56.7 -p tcp --dport 1723 -j DNAT --to 12.34.56.1
iptables -t nat -A POSTROUTING -s 12.34.56.1 -p gre -j SNAT --to 12.34.56.7
のように設定すると、pptpサーバを12.34.56.7で公開できます。 (必要に応じてインターフェースの指定やその他のフィルタリングも忘れずに。) GREプロトコルはサーバ側から張るので上記のSNATの指定だけが必要となります。 また、カーネルに対するpptp natパッチがありますが、 これは内側から発信された複数のpptpコネクションをmasquaradeするためのもので、 当てても当てなくても上記の指定が必要となります。 conntrack_ftpみたいに対応するgreを自動で開けたりしてくれるといいんですが、 現段階(2.4.19_rev1)では動作しませんでした。