IP制限機能実装の記録~Rails~

# はじめに
初めまして、私は現在リンクアンドモチベーションにてバックエンドエンジニアをしている中﨑です。
先日、私はIPアドレス制限機能の実装を任されましたが、お恥ずかしながらIPアドレスについてほとんど知りませんでした、、、
そんな私がリリース完了するまでに学んだことを記載していきたいなと思っています。
私と同じように「IPアドレスについてあんまり分かっていないな」という方や、「IPアドレス制限の実装をするよ」という方の参考になれば幸いです。


# 目次
- [基本知識](#基本知識)
- [IPアドレスに関わるライブラリ](#ipアドレスに関わるライブラリ)
- [IPアドレス制限を実装した時のこと](#ipアドレス制限を実装した時のこと)
    - [前提](#前提)
    - [IPアドレスの入力チェック（バリデーション）](#ipアドレスの入力チェックバリデーション)
    - [IP制限検証時の性能について](#ip制限検証時の性能について)
- [ドキュメント用に記載するIPアドレスについて](#ドキュメント用に記載するipアドレスについて)


# 基本知識
IPアドレスの基本的な知識を身につけるために、以下の記事を読ませていただきました
IPアドレスでよく出てくる用語に関して、わからない部分があれば個別のインプット記事を読むと良いと思います

#### 全体的なインプット
[【図解】IPアドレスの仕組み(クラス分類からサブネットまで)](https://nishinatoshiharu.com/fundamental-ipaddress/)
[IPアドレスの基礎知識](https://qiita.com/mogulla3/items/efb4c9328d82d24d98e6)

#### 個別のインプット
[IPアドレス](https://wa3.i-3-i.info/word172.html)
[CIDR表記](https://wa3.i-3-i.info/word11990.html)
[アドレスクラス](https://wa3.i-3-i.info/word11984.html)
[サブネットマスク](https://wa3.i-3-i.info/word11975.html)
[グローバルアドレス](https://wa3.i-3-i.info/word12831.html)と[プライベートアドレス](https://wa3.i-3-i.info/word11977.html)
[ネットワーク部](https://wa3.i-3-i.info/word17256.html)と[ホスト部](https://wa3.i-3-i.info/word17257.html)


# IPアドレスに関わるライブラリ
- [ipaddress](https://github.com/ipaddress-gem/ipaddress)
- ruby標準ライブラリの[ipaddr](https://docs.ruby-lang.org/ja/latest/class/IPAddr.html)

## ipaddressの基本的な機能
標準ライブラリのipaddrは割愛させていただきます

```ruby
require 'ipaddress'

# オブジェクトの作成
ipaddress = IPAddress("192.0.2.0/24")
=> #<IPAddress::IPv4:0x000000000421e8d8 @address="192.0.2.0", @prefix=24, @octets=[192, 0, 2, 0], @u32=3221225984>

# アドレスの取得
ipaddress.address
=> "192.0.2.0"

# prefixの取得
ipaddress.prefix
=> 24

# ネットマスクの取得
ipaddress.netmask
=> "255.255.255.0"

# cidr表記で取得(cidr表記でないものは/32が補完される)
ipaddress.to_string
=> "192.0.2.0/24"

# binaryでの取得
ipaddress.bits
=> "11000000000000000000001000000000"

# 32bitでの取得
ipaddress.to_u32
=> 3221225984

# ipv4形式→ipv6形式へ変換
ipaddress.to_ipv6
=> "c000:0200"

# IPv4クラスを指定してオブジェクトの作成
ipv4 = IPAddress::IPv4.new("192.0.2.0/24")
=> #<IPAddress::IPv4:0x00000000080fd1d0 @address="192.0.2.0", @prefix=24, @octets=[192, 0, 2, 0], @u32=3221225984>

# IPv6クラスを指定してオブジェクトの作成
ipv6 = IPAddress::IPv6.new("2001:db8::/32")
=> #<IPAddress::IPv6:0x0000000007f66e70 @groups=[8193, 3512, 0, 0, 0, 0, 0, 0], @address="2001:0db8:0000:0000:0000:0000:0000:0000", @compressed="2001:db8::", @prefix=32>
```
※より詳細な使用方法やメソッドについては[ドキュメント](https://github.com/ipaddress-gem/ipaddress)を参照ください


# IPアドレス制限を実装した時のこと
## 前提
取得したクライアントIPは完全に信用できるものではないので、あくまでアクセス制限するための1つの要素です。
今回はIP制限する際に必要なことの一部を記載しております。セキュアにクライアントIPを取得するための方法などは下記を参照ください。

参照
https://www.m3tech.blog/entry/x-forwarded-for
https://mrk21.hatenablog.com/entry/2020/08/06/214922


##IPアドレスの入力チェック（バリデーション）
主に、前章で記載したipaddressライブラリを用いて、どのようにバリデーションをしていくかを記載しております。
### IPのフォーマットとして正しいか
#### 不正なフォーマットでIPAddressのオブジェクトを作成しようとするとArgumentErrorが発生するのを利用した
IPv4/IPv6のフォーマットの違いの考慮や正規表現を使わなくていいのがありがたい、、、

```ruby
require 'ipaddress'

def validate_ip_format(ips)
  ips.each { |ip| IPAddress(ip) }
  true
rescue ArgumentError
  false
end 
```
#### 実行結果

```ruby
# IPの形式が正しいとき
ips = ["192.0.2.0/24", "198.51.100.0/24"]
validate_ip_format?(ips)
=> true

# 不正なフォーマットのとき
ips = ["192", "198.51.100.0/24"]
validate_ip_format?(ips)
=> false

ips = ["hoge", "198.51.100.0/24"]
validate_ip_format?(ips)
=> false

# prefixが不正なとき
ips = ["192.0.2.0/33", "198.51.100.0/24"]
validate_ip_format?(ips)
=> false
```

### 重複しているものがないか
#### cidr表記に変換してくれる"#to_string"メソッドを用いることで重複を除く
"192.0.2.0"と"192.0.2.0/32"を重複しているとみなしてくれるのがありがたい、、、

```ruby
require 'ipaddress'

def validate_ip_uniqueness(ips)
  check_ips = ips.map { |ip| IPAddress(ip).to_string }
  check_ips == check_ips.uniq
end
```

#### 実行結果

```ruby
# 重複がない時
ips = ["192.0.2.0/24", "198.51.100.0/24"]
validate_ip_uniqueness(ips)
=> true

# 重複があるとき
ips = ["192.0.2.0/24", "192.0.2.0/24"]
validate_ip_uniqueness(ips)
=> false

# 個別IPとcidr表記で重複があるとき
ips = ["192.0.2.0", "192.0.2.0/32"]
validate_ip_uniqueness(ips)
=> false
```



## IP制限検証時の性能について
お客様が登録したいIPアドレス`192.0.2.0/24`だと仮定する
※コードにすると複雑になるため割愛させていただいてます

### 悪い例
`192.0.2.0/24`の範囲は`192.0.2.0 ~ 192.0.2.255`だから1つ1つ合計256個保存する
（`***.***.***.***`のIPアドレスからアクセスがきた）
登録されている256個のIPアドレスの中に`***.***.***.***`が含まれているか検証する

○実際の住所のイメージ
登録された住所が`東京都中央区銀座1丁目〇〇 ~ 東京都中央区銀座8丁目△△`だから全部登録する
（`東京都****`の人がやってきた）
`東京都中央区銀座1丁目〇〇 ~ 東京都中央区銀座8丁目△△`の中にこの人が含まれているか検証する
※東京都銀座は1丁目から8丁目まで


#### 何が良くないのか
- 許可するIPをメモリにキャッシュしている場合
    - メモリにキャッシュする際に時間がかかってしまう
    - メモリにキャッシュするデータ容量が大きくなり、逼迫してしまう
- 検証時に時間がかかってしまい、ユーザー体験が悪くなってしまう

### 良い例
`192.0.2.0/24`のネットワーク部分は`110000000000000000000010`
（`***.***.***.***`のIPアドレスからアクセスがきた）
`***.***.***.***`のアドレスをbitに変換して、先頭24文字目までが`110000000000000000000010`と一致しているか検証する

○実際の住所のイメージ
登録された住所が`東京都中央区銀座1丁目〇〇 ~ 東京都中央区銀座8丁目△△`だから`東京都中央区銀座`かどうかを検証すればいい
（`東京都****`の人がやってきた）
`東京都中央区銀座`に一致しているか検証する

#### 何が良いか
- binaryでの検証になるので、速度が速い
- キャッシュするデータ容量が少なくて済む

# ドキュメント用に記載するIPアドレスについて
#### RFCによって定められている下記のIPを使用すると良い
IPv4

```
192.0.2.0/24
198.51.100.0/24
203.0.113.0/24
```

IPv6

```
2001:db8::/32
```

参照
[RFC5737 IPv4 Address Blocks Reserved for Documentation](https://datatracker.ietf.org/doc/html/rfc5737)
[RFC3849 IPv6 Address Prefix Reserved for Documentation](https://datatracker.ietf.org/doc/html/rfc3849)

# さいごに
ここまで、記事を読んでくださりありがとうございます。
私自身、エンジニアとして2年目になりますが、
お恥ずかしながら、IPアドレスの機能化の実装を任されて初めてちゃんとIPアドレスについての知識をインプットしました。
IPアドレスのようによく聞くけど、実はあんまりわかっていないことがまだまだたくさんあることにも気づかされました。例えばプロキシとか、、、、
私と同じように、聞いたことあるけど実はあまり知らないな。という人に少しでも参考になっていますと幸いです。

**[弊社アドベントカレンダー](https://qiita.com/advent-calendar/2021/mcs)ではたくさんの記事が投稿されています！よかったらご覧ください！**