C#でVPN設定を作る

## はじめに
作ったの自体は結構前ですが、せっかく作ったものなので人の目に見えるところに。。。

DotRasを使用したVPN設定の追加をする簡易ツールです。
最新のDotRas.for.Win10だとRC版までで正式版がでていなかったのでDotRas.for.Win8で作っています。
なので.NETではなく.NET Frameworkです(DotRas.for.Win10でも.NETは5までですが)。

また、VPNは事前共有キーありのL2TP/IPSecです。

## 使用ライブラリ
画面周り
* MahApps.Metro
* MaterialDesignThemes.MahApps
* Prism.Unity
* ReactiveProperty.WPF
* System.ComponentModel.Annotations
* System.Reactive

VPN周り
* DotRas.for.Win8

その他
* Nett
* Costura.Fody

## 画面イメージ
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/389699/88acb613-27b4-3489-0c51-18222f403fe9.png)

処理中はこんな感じになります。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/389699/5c4f07b0-df86-dff8-dc8b-86ee7d26ae0b.png)

## VPN設定の作成・テスト

CreateVpnL2TPAsyncでVPN設定を作ります。
ConnectTestAsyncでVPNが接続できるかテストしますがDotRasにテストするメソッドはないので、画面側から受け取った名前(VpnTest現在日時 の形式)で作ってDialで接続を行った上で接続可否をチェック、そして最後にRemoveEntryで削除します。

また、Asyncとついてますがこのメソッド自体は非同期になってないです。
画面でAsyncReactiveCommand使って非同期にしてます。

```csharp:RasPhoneEntry.cs
        public static CreateEntryResult CreateVpnL2TPAsync(string entryName, string userName, string password)
        {
            using (var book = new RasPhoneBookEx())
            {
                try
                {
                    if (!book.UsableL2TP()) return new CreateEntryResult(Properties.Resources.ErrorMessage002);

                    book.CreateL2TP(entryName, userName, password);

                    return new CreateEntryResult();
                }
                catch (Exception)
                {
                    return new CreateEntryResult(Properties.Resources.ErrorMessage005);
                }
            }
        }

        public static DialResult ConnectTestAsync(string entryName, string userName, string password)
        {
            using (var book = new RasPhoneBookEx())
            {
                using (var dialer = new RasDialerEx(entryName, userName, password))
                {
                    try
                    {
                        if (!book.UsableL2TP()) return new DialResult(Properties.Resources.ErrorMessage002);

                        book.CreateL2TP(entryName, userName, password);
                        dialer.Dial();

                        return new DialResult();
                    }
                    catch (Exception)
                    {
                        return new DialResult(Properties.Resources.ErrorMessage004);
                    }
                    finally
                    {
                        if (dialer.Opend) dialer.HangUp();
                        if (book.Contains(entryName)) book.RemoveEntry(entryName);
                    }
                }
            }
        }
```

CreateL2TPメソッドで実際にVPN接続を作ります。
entryのプロパティを色々変えれば色んなパターンに対応できると思います。

```csharp:RasPhoneBookEx.cs
        internal bool UsableL2TP()
        {
            return this.FindL2TPDevices().Any();
        }

        private IEnumerable<RasDevice> FindL2TPDevices()
        {
            return RasDevice.GetDevices().Where(d => d.Name.Contains("(L2TP)") && d.DeviceType == RasDeviceType.Vpn);
        }

        internal bool Contains(string entryName)
        {
            return this.Book.Entries.Contains(entryName);
        }

        internal void CreateL2TP(string entryName, string userName, string password)
        {
            // VPNのエントリ作成
            var device = this.FindL2TPDevices().First();
            var entry = RasEntry.CreateVpnEntry(entryName, VpnSettingsManager.ServerAddress, RasVpnStrategy.L2tpOnly, device);

            // 切断するまでの待ち時間(秒)
            // Windows標準
            entry.IdleDisconnectSeconds = 0;

            // 接続再試行回数
            // Windows標準
            entry.RedialCount = 3;

            // 接続再試行間隔(秒)
            // Windows標準
            entry.RedialPause = 60;

            // LCP拡張の無効化
            entry.Options.DisableLcpExtensions = false;

            // ソフトウェア圧縮の無効化
            entry.Options.SoftwareCompression = false;

            // 暗号化の有効化
            entry.EncryptionType = RasEncryptionType.Require;
            entry.Options.RequireEncryptedPassword = true;
            entry.Options.RequireDataEncryption = false;

            // マルチリンクの無効化
            entry.Options.DoNotNegotiateMultilink = true;

            // 使用しない認証プロトコルの無効化
            entry.Options.RequireChap = false;
            entry.Options.RequireEap = false;
            entry.Options.RequirePap = false;
            entry.Options.RequireMSChap = false;
            entry.Options.RequireMSChap2 = false;
            entry.Options.RequireMSEncryptedPassword = false;
            entry.Options.RequireSpap = false;
            entry.Options.RequireWin95MSChap = false;

            // 自動再接続の無効化
            entry.Options.ReconnectIfDropped = true;

            // 事前共有鍵を使用
            entry.Options.UsePreSharedKey = true;

            // VPN先のゲートウェイをデフォルトゲートウェイとして使用しない
            entry.Options.RemoteDefaultGateway = false;

            // 資格情報を保存
            entry.Options.CacheCredentials = true;

            this.Book.Entries.Add(entry);

            // 事前共有鍵を登録
            entry.UpdateCredentials(RasPreSharedKey.Client, VpnSettingsManager.PreSharedKey);

            // ユーザ名とパスワードを登録
            entry.UpdateCredentials(new NetworkCredential(userName, password));
        }

        internal void RemoveEntry(string entryName)
        {
            this.Book.Entries.Remove(entryName);
        }
```

このクラスで接続を実際に行います。

```csharp:RasDialerEx.cs
        internal RasDialer Dialer { get; }

        internal bool Opend { get; private set; }

        internal bool Closed { get; private set; }

        internal string EntryName { get; private set; }

        private RasHandle Handler;

        internal RasDialerEx(string entryName) : this(entryName, null, null) { }

        internal RasDialerEx(string entryName, string userName, string password)
        {
            this.EntryName = entryName;

            this.Dialer = new RasDialer
            {
                EntryName = entryName,
                PhoneBookPath = RasPhoneBook.GetPhoneBookPath(RasPhoneBookType.User),
                AllowUseStoredCredentials = true
            };

            if (userName is null && password is null)
            {
                this.Dialer.AllowUseStoredCredentials = true;
            }
            else
            {
                this.Dialer.Credentials = new NetworkCredential(userName, password);
            }
        }

        internal void Dial()
        {
            // ReactiveCommandAsyncで呼び出す前提とし、DialAsyncは使用しない
            this.Handler = this.Dialer.Dial();

            this.Opend = true;
            this.Closed = !this.Opend;
        }

        internal void HangUp()
        {
            var conn = RasConnection.GetActiveConnections().FirstOrDefault(c => c.EntryName == this.EntryName);
            conn?.HangUp();

            this.Opend = false;
            this.Closed = !this.Opend;
        }

        private void Close()
        {
            this.Handler?.Close();

            this.Dialer?.Dispose();
            this.Handler?.Dispose();
        }
```

## 接続設定
事前共有キー等はTOMLの設定ファイルに持っています。
これはNettを使って読み込みます。
TOML使ってるのは使ってみたかったからで深い意味はありません。

```csharp:TomlConfigurationLoader.cs
        public static bool LoadVpnConfiguration()
        {
            var path = Path.Combine(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), Properties.Resources.ConfigTomlFileName);

            if (File.Exists(path))
            {
                var toml = Toml.ReadFile(path);

                var vpnSettings = toml.Get<TomlTable>(nameof(VpnSettings));
                var serverAddress = vpnSettings.Get<string>(nameof(VpnSettings.ServerAddress));
                var preSharedKey = vpnSettings.Get<string>(nameof(VpnSettings.PreSharedKey));

                if (!(string.IsNullOrWhiteSpace(serverAddress) || string.IsNullOrWhiteSpace(preSharedKey)))
                {
                    VpnSettingsManager.InitializeSettings(serverAddress, preSharedKey);
                    return true;
                }
                else
                {
                    return false;
                }
            }
            else
            {
                CreateInitialConfigFile(path);

                return false;
            }
        }

        private static void CreateInitialConfigFile(string path)
        {
            var toml = Toml.Create();

            var vpnSettings = Toml.Create();
            vpnSettings.Add(nameof(VpnSettings.ServerAddress), string.Empty);
            vpnSettings.Add(nameof(VpnSettings.PreSharedKey), string.Empty);

            toml.Add(nameof(VpnSettings), vpnSettings);

            Toml.WriteFile(toml, path);
        }
```

```toml:conf.toml
[VpnSettings]
ServerAddress = "サーバアドレス"
PreSharedKey = "事前共有キー"
```

## dllの統合
通常、ビルドした後にできるファイルは各ライブラリやプロジェクトごとにdllができます。
ただ多くなると非常に煩雑なのでCostura.Fodyでまとめています。
おかげでexe1つになってすっきりします。

.Net Frameworkが終わって.NETになってからはVisual Studio標準機能でまとめることもできるようになったのでお手軽ですね。
リフレクションはどうもうまく動かなくなるようですが。

## GitHub
https://github.com/helvetica822/DotNet/tree/main