ASP.NET WebAPIで Port Knockingを作ってみた。

## Port Knocking?

システム管理すると、遠隔地のサーバーに管理者としてアクセスする必要があるが往々にある。sshやhttpsで暗号化された通路を使用するが、外部に常時さらされている管理ポートは、brute-force攻撃を受け常だ。

特にsshの場合は、ネット上にtcp/22万うろついボットが存在程度のリスクだが、fail2banやsshguardのような一定回数以上失敗した場合、自動的にシステムのファイアウォール（iptables、pf、firewalld...）を利用して、一定時間ブロックする手法が必要である。

もし常時サービスが必要としていない、すなわち、管理目的のサービスに見合った手法が「ポート - ノッキング（ [Port Knoking](https://en.wikipedia.org/wiki/Port_knocking)）」である。クライアントは、デーモンの事前約束した文字列を渡して、予め約束されたポートを少しの間だけ開いておく式である。こうするとポートが常に開放される必要はない。

## Basic API
提供するコマンドは以下の通り。

	request:
	POST -d '{action:"allow", ip:"127.0.0.1", port:8080}' /api/portkock
	POST -d '{action:"deny", ip:"127.0.0.1", port:8080}' /api/portkock

	response:
	when OKay, {"result":"OK"}
	when NG, {"result":"NG", "message":"bla bla ..."}

## 具現

referencesに "C:\Windows\System32\FirewallAPI.dll" を追加する必要がある。以下のようなシンプルなControllerを作ってみた。

```csharp

using PortKnockService;
using System;
using System.Runtime.Serialization;
using System.Web.Http;

namespace PortKockWebApi.Controllers
{
    public class PortKnockController : ApiController
    {

        [HttpGet]
        public PortKnockResponse DoGet(PortKnockRequest req)
        {
            return new PortKnockResponse()
            {
                Result = "NG",
                Message = "unknown method"
            };
        }


        [HttpPost]
        public PortKnockResponse DoPost(PortKnockRequest req)
        {

            // need to log here.

            if (req.Action.ToLower() == "allow") {
                try
                {
                    FirewallUtils.AllowAddressPort(req.Ip, req.Port);
                    return new PortKnockResponse()
                    {
                        Result = "OK",
                        Message = null
                    };

                }
                catch (Exception e)
                {
                    return new PortKnockResponse()
                    {
                        Result = "NG",
                        Message = e.Message
                    };
                }
            }
            else if (req.Action.ToLower() == "deny")
            {
                try
                {
                    FirewallUtils.CloseAddressPort(req.Ip, req.Port);
                    return new PortKnockResponse()
                    {
                        Result = "OK",
                        Message = null
                    };

                }
                catch (Exception e)
                {
                    return new PortKnockResponse()
                    {
                        Result = "NG",
                        Message = e.Message
                    };
                }
            }

            return new PortKnockResponse()
            {
                Result = "NG",
                Message = "unknown error"
            };
        }
    }

    [Serializable]
    [DataContract(Name = "")]
    public class PortKnockRequest
    {
        [DataMember(Name = "action", IsRequired = true)]
        public string Action { get; set; }

        [DataMember(Name = "ip", IsRequired = true)]
        public string Ip { get; set; }

        [DataMember(Name = "port", IsRequired = true)]
        public int Port { get; set; }
    }

    [Serializable]
    [DataContract(Name = "")]
    public class PortKnockResponse
    {
        [DataMember(Name = "result")]
        public string Result { get; set; }

        [DataMember(Name = "message", IsRequired =false)]
        public string Message { get; set; }

    }
}

```
## 具現したもの
https://github.com/crowdy/PortKnockingWebAPI

## 動作確認

実行する時にはFirewallを操作するため、管理者権限が必要だ。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/12587/11cf1705-a48b-949c-a670-1789e56eea29.png)

curlで実行して
![image.png](https://qiita-image-store.s3.amazonaws.com/0/12587/f9f6cfa2-d441-7fc6-af24-0ecb6a60eca9.png)

Powershellで結果を確認した。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/12587/5cd99a71-0c9b-527d-bc3b-c3d304272b88.png)

## 改善点
- proper listing feature current opened destinations

	```
	list opened destinations
	request:
		GET /api/portkock[?items=50&page=1]
	response:
		{items:[
			{id="", requested_at="", expired_at="", destination={action:"allow", ip:"127.0.0.1", port:8080}, result:{}},
			...
		]}

- proper getting request specific item details feature
	
	```
	request:
		GET /api/portkock/<id>
	response:
		...

- proper logging feature
- proper persistent to records
- proper scheduling to automated expiration
- not to show IIS error page