iwate

A software developper in Japan github: https://github.com/iwate

I released QjsIpc 🎉

Learning WASM 4 ~ 8 の成果をライブラリ化して公開した。

JS側では今回作成したQuickJSの拡張モジュールでるipcモジュールを使用し、プロシージャを登録して、最終的にlistenメソッドを実行する

import * as ipc from 'ipc';

ipc.register('echo', function (payload) {
  return payload;
});

ipc.listen();

.NET側では、QjsIpcEngineのインスタンスを作成し、JSファイルと実行フォルダを指定してQuickJSを実行する。

実行した後は、InvokeAsyncメソッドでJS側に登録したプロシージャを実行できる。

using QjsIpc;

await using var engine = new QjsIpcEngine();

engine.Start(new QjsIpcOptions
{
    ScriptFileName = "main.js",
    AllowedDirectoryPath = Environment.CurrentDirectory,
});

var message = await engine.InvokeAsync<string>("echo", "Hello, World!");

Console.WriteLine(message);

.NET側にプロシージャを用意し、JS側から呼び出すこともできる。

.NET側にプロシージャホストとなるクラスを作成し、そのインスタンスをMethodHostオプションに設定する。
生やすメソッドは、値でもTaskでもValueTaskでも大丈夫。

public class Host
{
    public int HostAdd(int a, int b) => a + b;

    public Task<string> HostStringTask() => Task.FromResult("host value");

    public async Task<string> HostStringAsync() {
        await Task.Delay(100);
        return "host value";
    }

    public async ValueTask<string> HostStringValueAsync() {
        await Task.Delay(100);
        return "host value";
    }
}

public class Program
{
    public static async Task Main(string[] args)
    {
        await using var engine = new QjsIpcEngine();

        engine.Start(new QjsIpcOptions
        {
          ScriptFileName = "main.js",
          AllowedDirectoryPath = Environment.CurrentDirectory,
          MethodHost = new Host()
        });

        await engine.InvokeAsync('createMessage', "iwate");
    }
}

JS側ではipc.invokeメソッドで呼び出せる。戻り値はPromise。

import * as ipc from 'ipc';

ipc.register('createMessage', async function (name) {
  const value = ipc.invoke('HostAdd', 100, 200);
  return `Hi, ${name}. The result of HostAdd(100, 200) is ${value}`;
});

ipc.listen();

ベンチマークもとってみたが、想像通り速くはない。けど使えないほどではない。

自分の実装の中で遅い部分の検討はついてて、.NET<->JS間を1バイトずつやり取りしてるのでまあ遅い。

wasmのmemory使ってページ(64kb)ごとにやり取りすればもっと早くなるかな。今後に課題。

// * Summary *

BenchmarkDotNet=v0.13.1, OS=Windows 10.0.22000
11th Gen Intel Core i7-1165G7 2.80GHz, 1 CPU, 8 logical and 4 physical cores
.NET SDK=6.0.200-preview.21617.4
  [Host]     : .NET 6.0.1 (6.0.121.56705), X64 RyuJIT  [AttachedDebugger]
  DefaultJob : .NET 6.0.1 (6.0.121.56705), X64 RyuJIT


|           Method |       Mean |     Error |    StdDev |
|----------------- |-----------:|----------:|----------:|
|             Echo |   303.7 us |  28.22 us |  81.42 us |
|        HostValue |   456.2 us |  41.25 us | 120.97 us |
|              Ejs | 1,414.2 us |  91.87 us | 269.43 us |
| EjsWithHostValue | 1,499.9 us | 121.33 us | 340.22 us |

Learning WASM #8

前回作ったqjs.wasmとJSON-RPCするプログラムを改良して、dotnetからjsへrpcコール中に、jsからdotnetにrpcコールできるようにした。

これでホスト側に生やしたヘルパー関数をゲスト関数が使える。

もう書いたコードが長くなってきたので全部貼ることはしないけど、いつも通りリポジトリにあげてある。

こんなホスト関数を生やしたクラスを用意して、

public class Host
{
    public async Task<int> HostMethod1(int intParam, float floatParam, string stringParam, ObjParam objParam)
    {
        await Task.Delay(1);
        Console.WriteLine("Invoke HostMethod1");
        Console.WriteLine($"intParam = {intParam}");
        Console.WriteLine($"floatParam = {floatParam}");
        Console.WriteLine($"stringParam = {stringParam}");
        Console.WriteLine($"objParam = {objParam?.Property}");
        return -1;
    }
}

public class ObjParam
{
    public string Property { get; set; }
}

QjsRpcのStartメソッドに渡すと、JS側から呼べるようにしてある。

var dir = AppDomain.CurrentDomain.BaseDirectory;
var rpc = new QjsRpc(dir);
var host = new Host();

rpc.Start(host);

var transformed = await rpc.InvokeAsync<JObject>("transform", new[] { new { name = "iwate" } }, CancellationToken.None);

Console.WriteLine(transformed);

await rpc.CloseWaitAsync();

JS側はdotnet側から呼ぶtransform関数が登録してあって、その中でホストのHostMethod1関数を呼んでいる。
Promiseが解決したら、EJSを実行して、HostMehtod1の結果と混ぜてホストに値を返す。

...
register('transform', function (payload, callback) {
  invoke('HostMethod1', 1, 1.0, 'hello', {property:'propvalue'}).then(function (value) {
    ejs.renderFile('template.ejs', payload, {}, function (err, str) {
      if (err) {
        std.err.puts(err);
      }
      else {
        const transformed = {
          name: payload.name,
          html: str,
          value,
        };
        callback(transformed);
      }
    });
  });
});
...

実行結果はこんな感じ。

Invoke HostMethod1
intParam = 1
floatParam = 1
stringParam = hello
objParam = propvalue
{
  "name": "iwate",
  "html": "<html>\r\n<body>\r\n<p>Hello, Wasmtime! I am iwate.</p>\r\n</body>\r\n</html>",
  "value": -1
}

HostMethod1の出力もあるし、transformedにHostMethod1の結果であるvalue:-1もセットされている。大成功。

いい感じにまとまってきたので、次回はライブラリにしてNuGetに上げようかな。

Learning WASM #7

前回はqjsのカスタモジュールにreadメソッドとwriteメソッドを生やして、hostのdotnetランタイムと文字列のやり取りをできるようにした。

今回は、文字列がやり取りできるようになったのでJSON-RPCを流して、dotnetからjsの関数をコールしてその結果を得たい。

RPC処理ロジックをカスタムモジュールにするには、Cで書く必要がありちょっと大変なので、いったんmain.jsの中にJavaScriptで書いた

import * as host from 'host';
import * as std from 'std';
import ejs from './ejs.mjs';
ejs.fileLoader = function(filename) {
    const file = std.open(filename,'r');
    return file.readAsString();
};

const handlers = {};
function register(methodName, handler) {
    handlers[methodName] = handler;
}
function listen() {
    while(true) {
        const line = host.readLine();

        if (line == '.quit') {
            break;
        }

        const data = JSON.parse(line);
        const func = handlers[data.method];
        if (typeof func == 'function' && Array.isArray(data.params)) {
            func.apply(null, data.params.concat([function (result) {
                host.writeLine(JSON.stringify({
                    jsonrpc: '2.0',
                    result,
                    id: data.id,
                }));
            }]));
        }
    }
}
register('transform', function (payload, callback) {
    ejs.renderFile('template.ejs', payload, {}, function (err, str) {
        if (err) {
            std.err.puts(err);
        }
        else {
            const transformed = {
                name: payload.name,
                html: str
            };
            callback(transformed);
        }
    })
});

listen();

関数を登録する registerとhostとのやり取りを開始するlistenを作成した。 json-rpcにはないが、一応終了させるための .quitを受信するとlistenが終了しプログラムが終了する。

dotnetは前回と同じ、ペイロードとなる rbufの初期値だけ違う

var rpccall = JsonSerializer.Serialize(new
{
    jsonrpc = "2.0",
    method = "transform",
    @params = new[] { new { name = "iwate"} },
    id = Guid.NewGuid().ToString()
});

using var rbuf = new MemoryStream(Encoding.UTF8.GetBytes($"{rpccall}\n.quit\n"));
using var wbuf = new MemoryStream();

using var engine = new Engine();
using var module = Module.FromFile(engine, "qjs.wasm");
using var linker = new Linker(engine);
using var store = new Store(engine);

linker.DefineFunction("env", "read", () => {
    return (int)rbuf.ReadByte();
});

linker.DefineFunction("env", "write", (int c) => {
    wbuf.WriteByte((byte)c);
});

linker.DefineWasi();

store.SetWasiConfiguration(new WasiConfiguration()
    .WithPreopenedDirectory(dir, ".")
    .WithInheritedStandardOutput()
    .WithArgs("--", "main.js"));

var instance = linker.Instantiate(store, module);

var run = instance.GetFunction(store, "_start");

run.Invoke(store);

wbuf.Seek(0, SeekOrigin.Begin);
var str = Encoding.UTF8.GetString(wbuf.ToArray());
Console.WriteLine(str);

実行結果はこんな感じ、EJSの実行結果がJSON-RPC形式で返ってきてる。

{"jsonrpc":"2.0","result":{"name":"iwate","html":"<html>\r\n<body>\r\n<p>Hello, Wasmtime! I am iwate.</p>\r\n</body>\r\n</html>"},"id":"e4c82100-a5b8-4389-bcbf-41b77682f311"}

次は、dotnet側をTask化して、main.jsをlisten状態で常駐させておいて、任意のタイミングで呼び出せるようにしたい。
それか、qjs側のJSON-RPC処理部のCモジュール化とJSON-RPCエラー系の実装か。

どれ最初にやろうかな。まよう。

自由にjs処理を呼べる下地ができてきていよいよ楽しくなってきた。

Learning WASM #6

前回はqjsからhostの関数を呼び出した。

今回は、実際に通信できるように read と write を生やしてみる。

C側は puts や getLine を参考にこんな風に実装。dotnet側には1byteずつread writeする関数を生やして、jsモジュールとしては改行コードもしくはEOFまで書き込み読み込みし続けるreadLine、writeLineとして実装する。

__attribute__((import_name("read")))extern int host_read();
__attribute__((import_name("write")))extern void host_write(int c);

static JSValue js_host_read_line(JSContext *ctx, JSValueConst this_val, 
                                int argc, JSValueConst *argv)
{
    int c;
    DynBuf dbuf;
    JSValue obj;

    js_std_dbuf_init(ctx, &dbuf);
    for(;;) {
        c = host_read();
        if (c == EOF) {
            if (dbuf.size == 0) {
                /* EOF */
                dbuf_free(&dbuf);
                return JS_NULL;
            } else {
                break;
            }
        }
        if (c == '\n')
            break;
        if (dbuf_putc(&dbuf, c)) {
            dbuf_free(&dbuf);
            return JS_ThrowOutOfMemory(ctx);
        }
    }
    obj = JS_NewStringLen(ctx, (const char *)dbuf.buf, dbuf.size);
    dbuf_free(&dbuf);
    return obj;
}

static JSValue js_host_write_line(JSContext *ctx, JSValueConst this_val, 
                                int argc, JSValueConst *argv)
{
    int i,j;
    size_t len;
    const char *str;

    for(i = 0; i < argc; i++) {
        str = JS_ToCStringLen(ctx, &len, argv[i]);
        if (!str)
            return JS_EXCEPTION;

        for (j = 0; j < len; j++) {
            host_write(str[j]);
        }

        host_write('\n');

        JS_FreeCString(ctx, str);
    }
    return JS_UNDEFINED;
}

static const JSCFunctionListEntry js_host_funcs[] = {
    JS_CFUNC_DEF("readLine", 0, js_host_read_line),
    JS_CFUNC_DEF("writeLine", 1, js_host_write_line),
};

static int js_host_init(JSContext *ctx, JSModuleDef *m)
{
    return JS_SetModuleExportList(ctx, m, js_host_funcs,
                                  countof(js_host_funcs));
}

JSModuleDef *js_init_module_host(JSContext *ctx, const char *module_name)
{
    JSModuleDef *m;
    m = JS_NewCModule(ctx, module_name, js_host_init);
    if (!m)
        return NULL;
    JS_AddModuleExportList(ctx, m, js_host_funcs, countof(js_host_funcs));
    return m;
}

dotnet側はとりあえずStreamに直結させるリングバッファとか考えたけどこれが手っ取り早い

using System;
using System.IO;
using System.Text;
using Wasmtime;

var dir = AppDomain.CurrentDomain.BaseDirectory;
var script =
@"
import * as host from 'host';
console.log('-- start js');
host.writeLine('Hello, I\'m js!');
const str = host.readLine();
console.log(str);
console.log('-- end js');

";
File.WriteAllText(Path.Combine(dir, "main.js"), script);

using var rbuf = new MemoryStream(Encoding.UTF8.GetBytes("Hello, I'm dotnet!\n"));
using var wbuf = new MemoryStream();

using var engine = new Engine();
using var module = Module.FromFile(engine, "qjs.wasm");
using var linker = new Linker(engine);
using var store = new Store(engine);

linker.DefineFunction("env", "read", () => {
    return (int)rbuf.ReadByte();
});

linker.DefineFunction("env", "write", (int c) => {
    wbuf.WriteByte((byte)c);
});

linker.DefineWasi();

store.SetWasiConfiguration(new WasiConfiguration()
    .WithPreopenedDirectory(dir, ".")
    .WithInheritedStandardOutput()
    .WithArgs("--", "main.js"));

var instance = linker.Instantiate(store, module);

var run = instance.GetFunction(store, "_start");

run.Invoke(store);

wbuf.Seek(0, SeekOrigin.Begin);
var str = Encoding.UTF8.GetString(wbuf.ToArray());
Console.WriteLine(str);

実行結果はこんな感じ。 qjs内でdotnetから読み込んだ文字列を標準出力して、実行完了後、dotnet側でjsから受け取った文字列を標準出力してる。

-- start js
Hello, I'm dotnet!
-- end js
Hello, I'm js!

これで、文字列のやり取りができるようになったので、JSON RPCはできそう。

ただ、ループで回してたりするからjsからdotnetにRPCしている中でdotnetからjsにRPCするような、行ったり来たりするRPCはできそうにない。

まあそれは出来なくても大丈夫なはず。

次はJSON RPCを組んでみようと思う。

Learning WASM #5

前回、dotnet上で動かしたwasmtimeの上でJS(QuickJS)を動かした。(ややこしいな

QuickJSの性質上、QuickJSで動いているJSとホストであるdotnetで通信させる方法が、ファイルを返した通信に限られる。

標準入出力でも行けないかと思ったけど、process分離して動いてるわけじゃないから標準入出力を共有してるためフックできなかった。

Cのfopenが実装されているので、ファイルだけでなくUNIXソケットファイルとかでも通信可能そうだったけどそうなるとホストがUNIXやLinuxじゃないとだめなので、開発しにくそうでやめた。おんなじ理由でtmpfsを使ったRAM上の通信もやめた。

せっかく、WASMを使ってるんだから、QucikJSに手を入れてホストと通信する関数をはやしてしまえばいいだろうということで書いてみた。

https://github.com/iwate/learn-wasm-with-dotnet/tree/main/challenge05

今回は簡単にwasmtimeのサンプルも記載されている引数なしのhello関数をホストに生やした。

using var engine = new Engine();
using var linker = new Linker(engine);
using var store = new Store(engine);

linker.DefineFunction("env", "hello", () => { Console.WriteLine("This is called from wasm."); });

QuickJSはCで書かれているので(しかもファイル数が少ない)、カスタムが簡単。

外部の関数を取り込むにはCの作法どうり、externで使いたい関数のシンボルを定義

extern void hello();

↓の感じでモジュールとして拡張できればかっちょいいなと思ったので、

import * as host from 'host'
host.hello();

quickjs_libc.cに他のモジュールを参考にhostモジュールを追加する

extern void hello();

static JSValue js_host_hello(JSContext *ctx, JSValueConst this_val, 
                                int argc, JSValueConst *argv)
{
    hello();
    return JS_UNDEFINED;
}

static const JSCFunctionListEntry js_host_funcs[] = {
    JS_CFUNC_DEF("hello", 0, js_host_hello),
};

static int js_host_init(JSContext *ctx, JSModuleDef *m)
{
    return JS_SetModuleExportList(ctx, m, js_host_funcs,
                                  countof(js_host_funcs));
}

JSModuleDef *js_init_module_host(JSContext *ctx, const char *module_name)
{
    JSModuleDef *m;
    m = JS_NewCModule(ctx, module_name, js_host_init);
    if (!m)
        return NULL;
    JS_AddModuleExportList(ctx, m, js_host_funcs, countof(js_host_funcs));
    return m;
}

ただ、こんなエラーが出てすんなりとはビルドがうまくいかない。

wasm-ld: error: CMakeFiles/qjs.dir/src/quickjs-libc.c.obj: undefined symbol: hello

さっきのexternで定義したホストの関数が特定できない。実行時にリンクされるから、この段階はundefinedでいいんだけど無視してくれない。

wasm-ldのオプション調べたらどうやら --allow-undefined をつければいいらしく、CMakeLists.txtに設定を足す

SET(CMAKE_EXE_LINKER_FLAGS  "${CMAKE_EXE_LINKER_FLAGS} --allow-undefined")

でもそれでもエラー。なぜか無視してくれないから頭を抱えたけど、カンマ区切りで付与することに途中で気づいた。CMakeむずい。

SET(CMAKE_EXE_LINKER_FLAGS  "${CMAKE_EXE_LINKER_FLAGS},--allow-undefined")

無事ビルドできたqjs.wasmを実行する

using System;
using System.IO;
using Wasmtime;

var dir = AppDomain.CurrentDomain.BaseDirectory;
var script =
@"
import * as host from 'host';
host.hello();
";
File.WriteAllText(Path.Combine(dir, "main.js"), script);

using var engine = new Engine();
using var module = Module.FromFile(engine, "qjs.wasm");
using var linker = new Linker(engine);
using var store = new Store(engine);

linker.DefineFunction("env", "hello", () => { Console.WriteLine("This is called from wasm."); });

linker.DefineWasi();

store.SetWasiConfiguration(new WasiConfiguration()
    .WithPreopenedDirectory(dir, ".")
    .WithArgs("--", "main.js"));

var instance = linker.Instantiate(store, module);

var run = instance.GetFunction(store, "_start");

run.Invoke(store);

実行結果はもちろん

This is called from wasm.

やったね。hostの関数を呼び出せた。これでファイルを介さずに通信できる、ホスト機能やデータが使える。

Learning WASM #4

wapmでqjsっていうパッケージを見つけた。

知らなかったのだけど、QuickJSっていう軽量なJSランタイムがあってそれをwasm化したものだった。

https://bellard.org/quickjs/

wapmに上がってるのは公式じゃなくて、サードパーティー。しかもバージョンが2年くらい古い

せっかくだから、最新ものにコンパイルしようと試みたけれど、ソース改変なしにはうまくいきそうになかったのでいったんwapmにあるもので遊んでみた。

WithArgs に渡す値ではまったけど動いた。

using System;
using System.IO;
using System.IO.MemoryMappedFiles;
using System.Text;
using Wasmtime;

namespace Iwate.Challenge04
{
    class Program
    {
        static void Main(string[] args)
        {
            var dir = AppDomain.CurrentDomain.BaseDirectory;
            var scriptArg = "{\"id\":100,\"name\":\"iwate\"}";
            var script =
@"
import * as std from 'std';
import ejs from 'ejs.esm.js'

ejs.fileLoader = function(filename) {
    const file = std.open(filename,'r');
    return file.readAsString();
};

const original = JSON.parse(scriptArgs[1]);

ejs.renderFile('template.ejs', original, {}, function (err, str) {
    if (err) {
        std.err.puts(err);
    }
    else {
        const transformed = {
            id: original.id,
            html: str
        };

        console.log(JSON.stringify(transformed));
    }
})
";
            File.WriteAllText(Path.Combine(dir, "main.js"), script);

            using var engine = new Engine();
            using var module = Module.FromFile(engine, "qjs.wasm");
            using var linker = new Linker(engine);
            using var store = new Store(engine);

            linker.DefineWasi();

            store.SetWasiConfiguration(new WasiConfiguration()
                .WithEnvironmentVariable("WASMTIME_BACKTRACE_DETAILS", "1")
                .WithInheritedStandardInput()
                .WithInheritedStandardOutput()
                .WithInheritedStandardError()
                .WithPreopenedDirectory(dir, ".")
                .WithArgs("--", "-m", "--std", "main.js", scriptArg));

            var instance = linker.Instantiate(store, module);

            var run = instance.GetFunction(store, "_start");


            run.Invoke(store);
        }
    }
}

wasmの引数にJSONを渡して、JS内で値を加工、標準出力で出力って感じにした。

単に加工するだけじゃつまらないのでejsを使ってHTMLに加工してみた。

実行結果はこんな感じ。

{"id":100,"html":"<html>\r\n<body>\r\n<p>Hello, Wasmtime! I am iwate.</p>\r\n</body>\r\n</html>"}

次は、wasmで動いてるjs内とdotnetホストを通信させる方法を考えたい。

Compile dotnet to wasm

.NETをwasmにAOTしてみる。おそらくランタイムごとwasmになるんかな

ひとまずこの人の説明通りやってみる。
https://stackoverflow.com/questions/70474778/compiling-c-sharp-project-to-webassembly

まず、emscriptenをいれる。Python3.6が必要ぽいけど入っていたのでそこはスキップ

git clone https://github.com/emscripten-core/emsdk.git
cd emsdk
./emsdk install latest
./emsdk activate latest

環境変数がセットされたので一応サインアウトからのサインイン。

適当なフォルダでdotnet projectをつくって、nugetconfigでnugetリポジトリを追加する。3rdPartyせいなのね。blazorのとかこれじゃなかったのか。

mkdir sample
cd sample
dotnet new classlib
dotnet new nugetconfig
<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <config>
    <add key="globalPackagesFolder" value=".packages" />
  </config>
  <packageSources>
    <!--To inherit the global NuGet package sources remove the <clear/> line below -->
    <clear />
    <add key="dotnet-experimental" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-experimental/nuget/v3/index.json" />
    <add key="nuget" value="https://api.nuget.org/v3/index.json" />
  </packageSources>
</configuration>

Class1にメソッドを追加

    [System.Runtime.InteropServices.UnmanagedCallersOnly(EntryPoint = "Answer")]
    public static int Answer()
    {
        return 41;
    }

csprojに以下を追加

  <ItemGroup>
    <PackageReference Include="Microsoft.DotNet.ILCompiler.LLVM" Version="7.0.0-*" />
    <PackageReference Include="runtime.win-x64.Microsoft.DotNet.ILCompiler.LLVM" Version="7.0.0-*" />
  </ItemGroup>

次のコマンドでコンパイル

dotnet publish /p:NativeLib=Static /p:SelfContained=true -r browser-wasm -c Debug /p:TargetArchitecture=wasm /p:PlatformTarget=AnyCPU /p:MSBuildEnableWorkloadResolver=false /p:EmccExtraArgs="-s EXPORTED_FUNCTIONS=_Answer%2C_CoreRT_StaticInitialization -s EXPORTED_RUNTIME_METHODS=cwrap" --self-contained

するんだけどうまくいかない。こんなエラーが出る。

Microsoft (R) Build Engine version 17.1.0-preview-21610-01+96a618ca5 for .NET
Copyright (C) Microsoft Corporation. All rights reserved.

  Determining projects to restore...
  All projects are up-to-date for restore.
  You are using a preview version of .NET. See: https://aka.ms/dotnet-core-preview
  sample -> C:\Users\iwate\works\iwate\sample\bin\Debug\net6.0\browser-wasm\sample.dll
C:\Program Files\dotnet\sdk\6.0.200-preview.21617.4\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.Publish.targets(248,5): error MSB3030: Could not copy the file "bin\Debug\net6.0\browser-wasm\native\sample.html" because it was not found. [\path\to\sample\sample.csproj]

StackOverflowじゃなくて、リポジトリのドキュメントを参考にしてみる。
https://github.com/dotnet/runtimelab/blob/feature/NativeAOT-LLVM/docs/using-nativeaot/compiling.md#webassembly

今度は、consoleアプリでそのままアプリケーションにする

dotnet new console
dotnet new nugetconfig

nuget.config

<?xml version="1.0" encoding="utf-8"?>
<configuration>
  <packageSources>
    <!--To inherit the global NuGet package sources remove the <clear/> line below -->
    <clear />
    <add key="dotnet-experimental" value="https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet-experimental/nuget/v3/index.json" />
    <add key="nuget" value="https://api.nuget.org/v3/index.json" />
  </packageSources>
</configuration>

csproj

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net6.0</TargetFramework>
    <ImplicitUsings>enable</ImplicitUsings>
    <Nullable>enable</Nullable>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.DotNet.ILCompiler" Version="7.0.0-*" />
    <PackageReference Include="Microsoft.DotNet.ILCompiler.LLVM; runtime.win-x64.Microsoft.DotNet.ILCompiler.LLVM" Version="7.0.0-*" />
  </ItemGroup>

</Project>

ビルド

dotnet publish -r browser-wasm -c Debug /p:TargetArchitecture=wasm /p:PlatformTarget=AnyCPU

workdloadが足りないって言われる

C:\Program Files\dotnet\sdk\6.0.200-preview.21617.4\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.Sdk.ImportWorkloads.targets(38,5): error NETSDK1147: To build this project, the following workloads must be installed: wasm-tools 
C:\Program Files\dotnet\sdk\6.0.200-preview.21617.4\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.Sdk.ImportWorkloads.targets(38,5): error NETSDK1147: To install these workloads, run the following command: dotnet workload install wasm-tools

ので足す。

dotnet workload install wasm-tools

再度ビルドするがエラー

$ dotnet publish -r browser-wasm -c Debug /p:TargetArchitecture=wasm /p:PlatformTarget=AnyCPU
Microsoft (R) Build Engine version 17.1.0-preview-21610-01+96a618ca5 for .NET
Copyright (C) Microsoft Corporation. All rights reserved.

  Determining projects to restore...
  Restored C:\Users\iwate\works\iwate\wasm\wasm.csproj (in 1.6 min).
  You are using a preview version of .NET. See: https://aka.ms/dotnet-core-preview
C:\Program Files\dotnet\sdk\6.0.200-preview.21617.4\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.Sdk.targets(1110,5): warning NETSDK1179: One of '--self-contained' or '--no-self-contained' options are required when '--runtime' is used. [C:\Users\iwate\works\iwate\wasm\wasm.csproj]
  wasm -> C:\Users\iwate\works\iwate\wasm\bin\Debug\net6.0\browser-wasm\wasm.dll
C:\Program Files\dotnet\packs\Microsoft.NET.Runtime.WebAssembly.Sdk\6.0.2-mauipre.1.22054.8\Sdk\WasmApp.targets(270,5): error : $(WasmMainJSPath) property needs to be set 

プロジェクト構成見てみる
https://github.com/dotnet/runtimelab/tree/feature/NativeAOT-LLVM/samples/HelloWorld

dotnet6じゃなくて5だったので、5に変更。Program.csも5になるように、namespaceやらエントリーポイントで囲う。

PackageReferenceも一つでいいぽい

<Project Sdk="Microsoft.NET.Sdk">

  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>net5.0</TargetFramework>
  </PropertyGroup>

  <ItemGroup>
    <PackageReference Include="Microsoft.DotNet.ILCompiler.LLVM; runtime.win-x64.Microsoft.DotNet.ILCompiler.LLVM" Version="7.0.0-*" />
  </ItemGroup>

</Project>

再度ビルド

$ dotnet publish -r browser-wasm -c Debug /p:TargetArchitecture=wasm /p:PlatformTarget=AnyCPU
Microsoft (R) Build Engine version 17.1.0-preview-21610-01+96a618ca5 for .NET
Copyright (C) Microsoft Corporation. All rights reserved.

  Determining projects to restore...
  Restored C:\Users\iwate\works\iwate\wasm\wasm.csproj (in 2.05 sec).
  You are using a preview version of .NET. See: https://aka.ms/dotnet-core-preview
  wasm -> C:\Users\iwate\works\iwate\wasm\bin\Debug\net5.0\browser-wasm\wasm.dll
  Generating compatible native code. To optimize for size or speed, visit https://aka.ms/OptimizeCoreRT
  RyuJIT compilation results, total methods 16830 RyuJit Methods 5699 33.8622%
C:\Program Files\dotnet\sdk\6.0.200-preview.21617.4\Sdks\Microsoft.NET.Sdk\targets\Microsoft.NET.Publish.targets(248,5): error MSB3030: Could not copy the file "bin\Debug\net5.0\browser-wasm\native\wasm.html" because it was not found. 

ズコー。最初と一緒やん。

Redash on Azure WebApps for Container

How to Provisioning

1. Create Azure Database for PostgreSQL server

2. Create Azure WebApps for Container

3. Allow WebApps outbound IPs in PostgreSQL's Connection security firewall

4. Configure compose settings for table provisioning in WebApps's DeployCenter

Configure following and execute create_db command.

version: '3.3'
services:
  redash:
    image: redash/redash:10.1.0.b50633
    environment:
      PYTHONUNBUFFERED: 0
      REDASH_LOG_LEVEL: INFO
      REDASH_COOKIE_SECRET: {self generated cookie secret}
      REDASH_DATABASE_URL: postgresql://postgres@{dbname}:{password}@{dbname}.postgres.database.azure.com:5432/postgres?sslmode=require
      REDASH_WEB_WORKERS: 4
    restart: always
    command: create_db

5. Configure compose redasn server in WebApps's DeployCenter

version: '3.3'
services:
  redis:
    image: redis:5.0-alpine
    restart: always
  scheduler:
    image: redash/redash:10.1.0.b50633
    environment:
      PYTHONUNBUFFERED: 0
      REDASH_LOG_LEVEL: INFO
      REDASH_REDIS_URL: redis://redis:6379/0
      REDASH_COOKIE_SECRET: {self generated cookie secret}
      REDASH_SECRET_KEY: {self generated secret}
      REDASH_DATABASE_URL: postgresql://postgres@{dbname}:{password}@{dbname}.postgres.database.azure.com:5432/postgres?sslmode=require
      QUEUES: "celery"
      WORKERS_COUNT: 1
    restart: always
    command: scheduler
  scheduled_worker:
    image: redash/redash:10.1.0.b50633
    environment:
      PYTHONUNBUFFERED: 0
      REDASH_LOG_LEVEL: INFO
      REDASH_REDIS_URL: redis://redis:6379/0
      REDASH_COOKIE_SECRET: {self generated cookie secret}
      REDASH_SECRET_KEY: {self generated secret}
      REDASH_DATABASE_URL: postgresql://postgres@{dbname}:{password}@{dbname}.postgres.database.azure.com:5432/postgres?sslmode=require
      QUEUES: "scheduled_queries,schemas"
      WORKERS_COUNT: 1
    restart: always
    command: worker
  adhoc_worker:
    image: redash/redash:10.1.0.b50633
    environment:
      PYTHONUNBUFFERED: 0
      REDASH_LOG_LEVEL: INFO
      REDASH_REDIS_URL: redis://redis:6379/0
      REDASH_COOKIE_SECRET: {self generated cookie secret}
      REDASH_SECRET_KEY: {self generated secret}
      REDASH_DATABASE_URL: postgresql://postgres@{dbname}:{password}@{dbname}.postgres.database.azure.com:5432/postgres?sslmode=require
      QUEUES: "queries"
      WORKERS_COUNT: 2
    restart: always
    command: worker
  redash:
    image: redash/redash:10.1.0.b50633
    environment:
      PYTHONUNBUFFERED: 0
      REDASH_LOG_LEVEL: INFO
      REDASH_REDIS_URL: redis://redis:6379/0
      REDASH_COOKIE_SECRET: {self generated cookie secret}
      REDASH_SECRET_KEY: {self generated secret}
      REDASH_DATABASE_URL: postgresql://postgres@{dbname}:{password}@{dbname}.postgres.database.azure.com:5432/postgres?sslmode=require
      REDASH_WEB_WORKERS: 4
    restart: always
    command: server
  nginx:
    image: redash/nginx:latest
    ports:
      - "80:80"
    restart: always

Why does it need to create Azure Database for PostgreSQL server

If you create PostgreSQL in docker-compose, you'll get this error.

FATAL: data directory "/var/lib/postgresql/data" has wrong ownership

Currently, Azure WebApps for Container has limitation for persisting storage. So, anybody don't up postgress in WebApps for Container

You cannot change permissions on the /home directory when persisting storage.
Applies to Web App for Containers
When you persist storage with the WEBSITESENABLEAPPSERVICESTORAGE app setting, we mount a location in Azure Storage to the /home mount point. The permissions on this are > 777. You cannot change these permissions, even if you attempt to do so from an initialization script or from SSH.
https://docs.microsoft.com/en-us/archive/blogs/waws/things-you-should-know-web-apps-and-linux#you-cannot-change-permissions-on-the-home-directory-when-persisting-storageapplies-to-web-app-for-containers

https://stackoverflow.com/questions/62384620/postgres-on-docker-in-azure

MAUI

MAUIはじめて見ようと思った。

とりあえず、環境構築から。

https://docs.microsoft.com/ja-jp/dotnet/maui/get-started/installation

VS2022が必要らしいので、そこからかー。

RCをインストール中

追記:
これ、RCでも本リリースでもだめでPreviewじゃないと今のところできない。

SaburaIIS

夏休みの自由研究(2か月もたっちゃったけど)として、こんなのを作った。

https://github.com/iwate/saburaiis

Azure VMSSのIISにContinuous Deliveryするためのシステムなんだけど、割と良くできていると思う。

挑戦的には、長いこと書いてなかったReactの今はどうなってるのか書いてみるってのが1つ、CosmosDBあんまり使ったことないから使ってみるてのが1つ、bicepも書いたことないから覚えて置こうってことで結構勉強になった。

作った理由はVMSS便利そうなんだけど、アプリケーションの配置がコレジャナイ感あったから。

VMSSへのアプリケーションのデプロイは以下の4つがメジャーっていう認識なんだけど、

  • カスタムプロビジョニングスクリプト
  • カスタムVHDイメージ
  • Service Fabric
  • Kubernets

カスタムプロビジョニングスクリプトやカスタムVHDイメージって新しいバージョンのアプリケーションをデプロイしたいとして、再起動なりVMの入れ替えなり(ローリングアップデート)が必要だから、1日に100回デプロイしたいみたいな運用には向かないと思うんですよ。
1つのアプリケーションだけなら、まだしも、せっかくVMでフルなIISが使えるのだから、複数のアプリケーション詰め込みたかったりするし、モジュラーモノリス的な使い方。
それには、合わないあって感じた。

他の2つSFとk8sはどうかって言うと、ちょっと重い。手元にあるzipに固めたアプリケーションが動けばいいのに、SFは専用のStatupコード書かなくちゃいけなかったり、コンテナイメージにしなくちゃいけなかったり、そうじゃない感

k8sはwindowsにホストされること前提のアプリケーション(.NET FrameworkとかWCFとか)には鬼門。Windows Containerをワーカーノードにできるって言っても、マスターノードはLinuxを用意しなくちゃいけない。ローカル開発できる環境整えるのが大変なのが応える。Docker Desktopで簡単に整えられるならいいんだけど、今んとこLinux or Windowsだから、LinuxとWindowsを併用しなきゃいけない構成ができない。やるならHyperVなりWSLなりで1から建てろと。

頑張ろうかなとも思ったけど、k8sとかトレンドだし、でもこのzipが展開できればそれでいいのにって感情が勝ったのです。

作ったものは、以下の3つ。

  • SaburaIIS Agent
  • SaburaIIS AdminWeb
  • SaburaIIS CLI

AgentはWindows Serviceとして作ってあって、VMSS内の各VMへプロビジョニングスクリプトでインストール・常駐させておく。

AdminWebとCLIは、設定データを編集するためのものでCosmosDBに入っているJSONをこねくり回す。

CosomosDBにはIISがとるべき姿がJSONで保存されていて、AgentはChangeFeedを監視して、何か変更があれば、差分を計算して適用する。VirtualDOMとDOMの関係に近くてAgentはReact的な役割をする。

SSL証明書は、Key Vaultに、アプリケーションプール毎に環境変数を設定したい時はApp Configurationをつかう。

とにかく、Azureのマネージドサービスを使い倒す方針。

とまあ、いい感じのができたと自分では思うわけです。
ここまで前置き、長かったけど。

他の人の反応が気になるから、Reditに投稿してみたというのが本題。

これ。

https://www.reddit.com/r/AZURE/comments/q4zkq1/github_iwatesaburaiis_a_continuous_delivery_agent/

これが、3k Viewぐらいあるのに、6voteしかなくてダメダメだなあって感じてます。

それで何がいけなかったかなという反省を書きたい。

ものは悪くないって思うので(そう思いたい)、Reditの投稿の仕方、見せ方が悪かったよねっての書き出していこうと思います。

まず、改めて読んでみると、なんだろう。Iが多いかな、セールストークやマーケティングメールにはYouを使えってどっかで読んだことがある。Youにどんなメリットがあるか、これが使えばYouの体験がこうよくなる適なことを書けてきな。

あと、最大の失敗な気がするのが、書いてある説明が、僕の作ったものを100%理解できる前提で、それでいて疑問に思うであろうことがらだけを説明してる。1%も理解してない人に対して説明するように書かないとダメだよねと反省

あと、作ったもの全部見せたいから動画の長さが30分になってしまってる。これじゃあ誰も全部見ないよなあ。僕でさえ見ない。

この反省踏まえて次のアクションは、SaburaIISのコア機能である一回の設定変更で各VMに一斉にアプリケーションを展開する動画を15秒ぐらいの短い尺で作って投稿してみようと思う。

ほっぽてるWASMもそろそろ再開しなきゃなあ

Learning WASM #3

Rustでライブラリを作成して、dotnetからそれを呼び出すのを試した。
https://github.com/iwate/learn-wasm-with-dotnet/tree/main/challenge02

まずは、簡単なとこから、ただの2値の足し算をするadd関数をrust -> wasm -> dotnetで実行する

rust + wasm で検索したら、wasm-packばっかりで、なかなかただのwasmの生成方法がわからなかったけど、wasiとかwasmtimeでググったら何とか知見が得られた。

まずは、rustでwasmをターゲットにしてコンパイルできるように、ターゲットを足す。

rustup target add wasm32-unknown-unknown

次に、普通にrustのプロジェクトを作る。add関数を外から使いたいからプロジェクトタイプはlibで。

Cargo.tomlはこんな感じでまっさらでいい。

[package]
name = "challenge02"
version = "0.1.0"
authors = ["iwate <github@iwate.me>"]
edition = "2018"

[dependencies]

wasmにするために唯一やらなくちゃいけないことは#[no_mangle]をつけるってこと。こういうのrustだとなんていうんだろ?dotnet的にattributeって言うのかな?それともjavaみたいにannotation?

#[no_mangle]
fn add(a: i32, b: i32) -> i32 {
    return a + b;
}

調べた。 attributeらしい。#がouter attributeで#!がinnter attributeだって。
https://doc.rust-lang.org/book/appendix-02-operators.html

あとは、ビルドするときにターゲットを指定する。 releaseビルドにしないと、wasmがデバッグ用のコードか何かですごく膨らむ。

cargo build --target=wasm32-unknown-unknown --release

あとは生成されたwasmをdotnetから使うだけ。

using System;
using Wasmtime;

using var engine = new Engine();
using var module = Module.FromFile(engine, "challenge02.wasm");
using var host = new Host(engine);

using dynamic instance = host.Instantiate(module);
var result = instance.add(1, 2);

Console.WriteLine(result);

コンパイルが面倒なので、ビルドイベントでrustのビルドとwasmのコピーもしちゃう。

<Project Sdk="Microsoft.NET.Sdk">

    <PropertyGroup>
        <OutputType>Exe</OutputType>
        <TargetFramework>net5.0</TargetFramework>
        <Nullable>enable</Nullable>
    </PropertyGroup>

    <ItemGroup>
        <PackageReference Include="Wasmtime" Version="0.27.0-preview1" />
    </ItemGroup>

    <ItemGroup>
      <None Update="challenge02.wasm">
        <CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
      </None>
    </ItemGroup>

    <Target Name="RustBuild" BeforeTargets="Build">
        <Exec WorkingDirectory="../rust" Command="cargo build --target=wasm32-unknown-unknown --release" />
        <Copy SourceFiles="../rust/target/wasm32-unknown-unknown/release/challenge02.wasm" DestinationFiles="./challenge02.wasm" />
    </Target>

</Project>

基本はできたので、次はwasm + protocol bufferを返してdotnetからrustを呼び出すコードを書く。

Learning WASM #2

Protocol Bufferフォーマットを使用して、dotnetとwasmの間でオブジェクトをやり取りするコードを書いてみた。
https://github.com/iwate/learn-wasm-with-dotnet/tree/main/challenge01

こんな感じのプロトファイルを用意して、

syntax = "proto3";

option csharp_namespace = "Iwate.WasmtimeChallenge";

package greet;

message Hello {
  string message = 1;
}

dotent側で生成した Hello をバイナリに書き出して、wasmのshared memoryに書き込んでから wasmモジュールのrun を実行する。
shared memoryから Helloをパースするとwasmで加工された情報が受け取れる。

using System;
using System.IO;
using Google.Protobuf;
using Iwate.WasmtimeChallenge;
using Wasmtime;

var bytes = new byte[32];
using (var stream = new MemoryStream(bytes))
{
    new Hello { Message = "Hello, " }.WriteDelimitedTo(stream);
    stream.Flush();
}


using var engine = new Engine();
using var module = Module.FromTextFile(engine, "memory.wat");
using var host = new Host(engine);
using var mem = host.DefineMemory("", "mem");

for (var i = 0; i < bytes.Length; i++)
{
    mem.WriteByte(100 + i, bytes[i]);
}

using dynamic instance = host.Instantiate(module);
instance.run();

var result = mem.Span.Slice(100, 32).ToArray();
using var stream1 = new MemoryStream(result);
var hello = Hello.Parser.ParseDelimitedFrom(stream1);
Console.WriteLine(hello.Message);
Console.WriteLine(Helpers.HexDump(result));

出力結果はこんな感じ。

wasmに渡す前は Hello,だったのが Hello,WASM! になって返ってきてる。

Hello,WASM!
00000000   0E 0A 0C 48 65 6C 6C 6F  2C 57 41 53 4D 21 00 00   ・・・Hello,WASM!・・
00000010   00 00 00 00 00 00 00 00  00 00 00 00 00 00 00 00   ・・・・・・・・・・・・・・・・

wasmはこんな感じ

(module
    (import "" "mem" (memory 1))
    (data (i32.const 0) "WASM!")
    (func $run
        (memory.copy (i32.add (i32.load8_u (i32.const 102)) (i32.const 102)) (i32.const 0) (i32.const 5))
        (i32.store8 (i32.const 102) (i32.add (i32.load8_u (i32.const 102)) (i32.const 5)))
        (i32.store8  (i32.const 100) (i32.add (i32.load8_u (i32.const 102)) (i32.const 2)))
    )
    (export "run" (func $run))
)

うまいこといった。shared memoryにコピーするのなんかバカっぽいなあ。最初からそっちのバイナリに書き込めればいいんだけど、Span<byte>をStreamにする方法がよくわからない。

次は、wasmをwasファイルからじゃなくて、rustとかで生成したやつで試そう。

rust用のprotocol bufferからシリアライズ、デシリアライズしたバイナリでdotnetと通信させてみたい。

WebApps as ARR

Last year, I describe WebApps with File Share for hosting static website.

I find another way.

WebApps can ARR if you transform applicationHost.

<!-- D:\home\site\applicationHost.xdt -->
<?xml version="1.0"?>
<configuration xmlns:xdt="http://schemas.microsoft.com/XML-Document-Transform">
  <system.webServer>
    <proxy xdt:Transform="InsertIfMissing"
      enabled="true"
      preserveHostHeader="false"
      reverseRewriteHostInResponseHeaders="false" />
  </system.webServer>
</configuration>
<!-- D:\home\site\wwwroot\web.config -->
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
    <system.webServer>
        <httpErrors errorMode="Detailed" />
        <urlCompression doStaticCompression="false" doDynamicCompression="false" />
        <rewrite>
            <rules>
                <rule name="BlobNotSlashEnd">
                    <match url="^.*[^/]$" />
                    <action url="https://storageaccount.blob.core.windows.net/containername{PATH_INFO}" type="Rewrite" />
                </rule>
                <rule name="BlobSlashEnd">
                    <match url="^.*/$" />
                    <action url="https://storageaccount.blob.core.windows.net/containername{PATH_INFO}index.html" type="Rewrite" />
                </rule>
            </rules>
        </rewrite>
    </system.webServer>
</configuration>

Learning WASM #1

dotnetからwasmを実行するのが面白そう。これがモノになったら多言語間をまたいだプラグインシステムが作るんじゃないかな。
まだ、File IOとかNetwork Stackとかは使えないらしいけど、専用関数用意すればよさそう。FastlyとかCloudflareとかはエッジ用のエンジンでwasm実行できるようにしてるしね。

まずは、wasmになれたい。

https://github.com/bytecodealliance/wasmtime-dotnet/blob/main/examples/memory/memory.wat

(module
  (type $t0 (func (param i32 i32)))
  (import "" "log" (func $.log (type $t0)))
  (memory (export "mem") 1 2)
  (data (i32.const 0) "Hello World")
  (func $run
    i32.const 0
    i32.const 11
    call $.log
  )
  (export "run" (func $run))
)

これはテキスト形式、本当はバイナリだけど、読みやすいテキスト形式で学習していく。

見た目は、S式だね。func $run ~ callのところを見た感じ、値、値、callと続いてるからスタックマシンぽいね。

https://developer.mozilla.org/en-US/docs/WebAssembly/Understanding_the_text_format

MDNのドキュメントを読みながら解読してく。

(module)

モジュールの塊を表す式(module)が最小のモジュールらしい。中身は空。

モジュールに入ってないコードは不正なんだろうか? module式で纏まってなかったらそれは文って感じだろうし違反しそう。

(func (param i32) (param i32) (result f64) ... )

モジュールに関数を生やすには、 func式を使う。 任意の(引数)、(戻り値)が続き、そのあと式の中身が続くぽい。

名前はどこだ? 一番最初のサンプルみるに、引数の前っぽいけど。名前の先頭に$がつくのは作法なのかな。

(module
    (func $myadd (param $l i32) (param $r i32) (result i32)
        local.get $l
        local.get $r
        i32.add)
)

こんな感じかな。引数は local.get でスタックに載せられるみたい。 i32.add はcallしなくていいから組み込み命令なのかな。

スタックマシンだから結果はスタックに乗る感じかな。

戻り値はどうやって指定するんだろ? スタックの一番上が自動的に返されるのかな。

便利なページ見つけた。
https://webassembly.github.io/spec/core/exec/index.html

これで構文がすぐわかるね。

作った関数は、 exportでモジュールのそとに公開できる。

(module
    (func $myadd (param $l i32) (param $r i32) (result i32)
        local.get $l
        local.get $r
        i32.add)
    (export "add" (func $myadd))
)

逆に外の関数を使えるようにするには import が必要。

import          ::= {module name, name name, desc importdesc}
importdesc  ::= func typeidxtable 
                        |       tabletypemem
                        |       memtypeglobal
                        |       globaltype

importにはモジュール名、対象名、説明が必要で説明の種類を見る限り、関数だけじゃななさそう。memoryとかglobalにも使う。tableはちょっとまだわからん。

プリミティブ以外の値はどうすればいいのか、memoryを使って、バッファーを確保して使うみたい。

js側からしたらArrayBufferをwasmに渡して、処理が終わったらそのArrayBufferからデータを取り出す感じ。

function consoleLogString(offset, length) {
  var bytes = new Uint8Array(memory.buffer, offset, length);
  var string = new TextDecoder('utf8').decode(bytes);
  console.log(string);
}

なるほどね。

(memory 1 2) みたいにかけるけど、これは、1が最小ページサイズ、2が最大ページサイズらしい。1ページ64KB。

この共有メモリにprotocol bufferとかmessage packのバイナリ置けば、言語間でオブジェクトをやり取りできそう。もちろんJSONでもいいわけだけど。

Forward Emailを使ってみる

https://forwardemail.net/ というナイスなサービスがある。

メール転送サービスで、SMTPにも対応しているので送信にもカスタムドメインが使える。しかも無料。

課金すると、どこに転送しているのかっていう情報を隠せる。例えばバックエンドをgmailで使用していたら、FREEでは、TXTレコードでフォワーディング先のgmailアカウントを設定する必要があるのでdigればわかってしまうのが、課金すればforwardemailのデータサーバに設定できるようになるので公開しなくて済む。

まあ、2段階認証してるし公開してもいいでしょう。ということで無料プランで利用することにした。

メインで使っているメールボックスはoutlook(今時少ないlive.jp)なので、最初はoutlookに転送していたけど、送信でつまずく。うまいこと、設定できずに、送信者がoutlookのメアドになってしまう。 カスタムDKIM設定できないし迷惑メール行きになるもんで困った。

https://forwardemail.net/faq?domain=iwate.me#lazyframe-MEheS8gM4Xs

この説明見る限り、エイリアスじゃなくて、外部のメールアドレスとして追加できればいけそうってのはわかったけど、outlookだとうまくいかず。しょうがないのでgmailで行くことにした。

設定は説明通りですぐに終わった。送信者もきっちりxxxx@iwate.meになってた。

でもメインはoutlookだから、普段はgmailの同期切ってるだよなあと思ったときに気づいた。

https://forwardemail.net/faq#can-i-forward-emails-to-multiple-recipients

複数の宛先に転送できるじゃん。

これでoutlookのほうにも転送設定することで、gmailにもoutlookにも届くようにできた。

これのいいところって、例えば、GoogleもしくはMicrosoftに不遇にもBANされたときに、メールだけは救える冗長性が手に入るってことに気づいた。認証メールとかもどっちにも届くからね。

https://gigazine.net/news/20210209-terraria-stadia-cancel/

クライアント証明書はIP制限の代替足りえるのか

Covid19でWFHが広く行われるようになって、1年たとうとしてしているがWFHでWeb上の管理画面を使用する機会を得た人も多かったことでしょう。
ただし、いままではWeb上の管理画面にはIP制限がかかっていることが多かったはずです。(何か資料で示せればいいのですが。)
WFHがはじまって、VPN終端に負荷がかかり、従来のプライベートネットワークによりセキュリティポリシーを見直し、ゼロトラストセキュリティという言葉も一般に広がりはじめました。
日本では、平井卓也デジタル改革担当大臣が行政の目指すセキュリティポリシーの指針として発言したことも広がるきっかけになりました。

さて、WFHで様々なIPから管理画面にアクセスしたいとした時に、今までのIP制限では都合が悪いのでクライアント証明書制限を検討しています。

しかし、私はクライアント証明書制限が本当にIP制限の代替なのか疑問に思っています。前提として、ID/パスワードでの認証がある管理画面にさらにIP制限をかけているケースで話しています。

  1. IP制限≒地理制限
    • IPは動的にしろ静的にしろISPによって決まります。さらにISPは国、地域ごとに割り振るIPレンジが決まっています。
    • 固定IPで払い出されてるIPを盗むには、
      • 物理的にLAN内に接続を試みる。(建物に侵入
      • VPNからLAN内に侵入する (VPN終端の脆弱性を狙う、VPNの認証情報を何らかの形で手に入れる
      • IP制限自体の脆弱性をつく(タイニー/オーバー フラグメント攻撃 未対応なサーバーはないと思うけど
  2. クライアント証明書はどうやって配りますか?
    • メールですか? 社内Wikiですか? (これってZipパスワード問題と同じですよね
    • ネットワークを介さない方法で配らないと意味なくないですか?(FIDO
    • OK. 証明書と署名を認証局サーバーでやるからダメなんだ、各PCで鍵を作成して、公開鍵だけを認証局に渡し署名する。
      • それ社員の全PCにやるサポートめっちゃ大変なやつやん

ちゃんとやれば(クライアント証明書は配らず、公開鍵を集める方式)IP制限足りえるけど、生成した鍵を配布するようなやり方でIP制限と同等のセキュリティ強度になるとは思えない。

配り方に気を付ければいいかもしれないけど。メールやWikiで配るのは絶対ダメで認証局まで取りに来てもらう。取りに来るときのセキュリティ強度がしっかりしてればOKなはず。

クライアント証明書の前に2要素認証なんじゃないかな。教えて詳しい人!