Python」カテゴリーアーカイブ

VPN 越しに DLNA 経由で NAS に触る (SSDP Proxy)

どうもみむらです。

VPN 越しに NAS のコンテンツに、しかも DLNA 対応アプリ経由で触りたいと思ったことはありませんか。

数ヶ月前に NAS を購入して、速攻で静音化の記事 ( ASUSTOR DataSync Center の HDD アクセスを静かにする ) を書いたりしていたのですが、DLNA 経由で音楽ファイルに触れるようにしてみた所これが大変使いやすく、ぜひ VPN 越しにもこれを享受したい・・と。。

DLNA は UPnP を用いており、その機器発見に SSDP (1900/udp) を用いているのですが、早い話が VPN 越しにブロードキャストが上手く動作しないので、これを改善してみようというそういう話です。


1.サーバから VPN 感を感じ取れないようにする

DLNA は同一ネットワーク上での動作を想定しているものになります。
他のネットワークからアクセスしようとすると上手く動いてくれないケースがいくつか見られましたので、下記のようにしてサーバからの 「VPN 感」を少し消してみます。

NAS 側には “192.168.1.0/24” のネットワークに属しているように見せ、VPN 用に “192.168.1.224/28” のネットワークを用意しておきます。

VPN サーバには Proxy ARP の設定を入れて、192.168.1.0/24 のネットワーク上からも 192.168.1.224/28 の端末の MAC アドレスの解決 (といっても VPN サーバが指し示されるだけ)が出来るようになります。

なお、Proxy ARP については、こちらのサイトが詳しいですので参照ください:
Proxy ARP(プロキシARP)とは / ネットワークエンジニアとして
https://www.infraexpert.com/study/gateway.htm


具体的な設定方法としては
“/proc/sys/net/ipv4/conf/<device>/proxy_arp” を 1 に書き換えるか
systemd-networkd の “[Network]” セクションに “IPv4ProxyARP=yes” とする方法が
簡単かなと思います。

設定する際は、“192.168.1.0/24” の方に ProxyARP の設定を適用する形になります。

systemd-networkd の設定例)

# cat /etc/systemd/network/eth0.network
[Match]
Name=eth0

[Network]
Address=192.168.1.240/24
Gateway=192.168.1.254
IPv4ProxyARP=yes

# cat /etc/systemd/network/eth1.network
[Match]
Name=eth1

[Network]
Address=192.168.1.238/28

[Link]
MTUBytes=1200

2. SSDP をプロキシする

探索プロトコルの通信を転送するようにしてみます。
転送しないと探索が VPN のネットワーク内で閉じてしまい、本来の機器まで届かなくなってしまいます。

SSDP の中身は概ね下記のような HTTP ベースの通信になっています。

# リクエストの例。(クライアントからサーバに対して送る)
# 239.255.255.250:1900/UDP に対して送出される
---
M-SEARCH * HTTP/1.1
MX: 5
ST: urn:schemas-upnp-org:device:MediaServer:1
MAN: "ssdp:discover"
User-Agent: UPnP/1.0
Host: 239.255.255.250:1900
Connection: close

---

# 応答例。
# サーバからリクエストを行ったクライアントのポートに対してユニキャストで応答が入る
---
HTTP/1.1 200 OK
Cache-Control: max-age=1800
EXT:
Location: http://192.168.1.22:55247/dms
Server: Linux 4.0.0 UPnP/1.0
ST: urn:schemas-upnp-org:device:MediaServer:1
USN: uuid:xxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxxx::urn:schemas-upnp-org:device:MediaServer:1
Date: Sun, 16 May 2021 19:42:50 GMT

---

見ていただくとわかるように、応答の中にユニキャストアドレスが含まれているため、発見処理のみプロキシしてあげれば後は1対1での通信となります。

ということで下記のようなプログラムを書いてみました

#!/bin/python3

#
# SSDP (DLNA の探索プロトコル) を吸って吐くプロキシ。
# Wireguard のように純粋にbridgeできないネットワーク間で使うと幸せになれる気がします。
# (使ったことでネットワークがダウン等しても保証は出来ませんので、自己責任でどうぞ。)
#
# Author : Satoshi Mimura (@mimura1133)
#

import socket
from contextlib import closing

def main():
        multicast_group = '239.255.255.250'
        src_adapter_ip = '' # DLNA クライアントがいるネットワークの IP アドレスを指定
        dst_adapter_ip = '' # DLNA サーバがいるネットワークの IP アドレスを指定
        port = 1900
        timeout = 5.0

        with closing(socket.socket(socket.AF_INET,socket.SOCK_DGRAM)) as src_sock:
            src_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR,1)
            src_sock.bind(('',port))
            src_sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP,
                                  socket.inet_aton(multicast_group) + socket.inet_aton(src_adapter_ip))

            while True:
                request_data, client_addr = src_sock.recvfrom(4096)
                with closing(socket.socket(socket.AF_INET,socket.SOCK_DGRAM)) as dst_sock:
                    dst_sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF, socket.inet_aton(dst_adapter_ip))
                    dst_sock.settimeout(timeout)
                    dst_sock.sendto(request_data,(multicast_group,port))
                    print("\033[31m [FORWARDED:REQUEST]\033[0m {} -> {}".format(client_addr,(multicast_group,port)))
                    print(request_data)
                    while True:
                        try:
                            response_data, server_addr = dst_sock.recvfrom(4096)
                            src_sock.sendto(response_data,client_addr)
                            print("\033[32m [FORWARDED:RESPONSE]\033[0m {} -> {}".format(server_addr,client_addr))
                            print(response_data)

                        except Exception:
                            break
        return

if __name__ == '__main__':
    main()

動かしてみた結果は下図の通り。中々上手く動いてます。

今回の検証には iPhone 側に OPlayer Lite を入れて検証しています。

3.永続化する

毎回手動で起動するのは筋がよくありませんので、systemd のサービスにしてしまいましょう。

まずは、先ほどの Proxy をするプログラムからデバッグ用の print 文を消去したものを準備します。

#!/bin/python3

#
# SSDP (DLNA の探索プロトコル) を吸って吐くプロキシ。
# Wireguard のように純粋にbridgeできないネットワーク間で使うと幸せになれる気がします。
# (使ったことでネットワークがダウン等しても保証は出来ませんので、自己責任でどうぞ。)
#
# Author : Satoshi Mimura (@mimura1133)
#

import socket
from contextlib import closing

def main():
        multicast_group = '239.255.255.250'
        src_adapter_ip = '' # DLNA クライアントがいるネットワークの IP アドレスを指定
        dst_adapter_ip = '' # DLNA サーバがいるネットワークの IP アドレスを指定
        port = 1900
        timeout = 5.0

        with closing(socket.socket(socket.AF_INET,socket.SOCK_DGRAM)) as src_sock:
            src_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR,1)
            src_sock.bind(('',port))
            src_sock.setsockopt(socket.IPPROTO_IP, socket.IP_ADD_MEMBERSHIP,
                                  socket.inet_aton(multicast_group) + socket.inet_aton(src_adapter_ip))

            while True:
                request_data, client_addr = src_sock.recvfrom(4096)
                with closing(socket.socket(socket.AF_INET,socket.SOCK_DGRAM)) as dst_sock:
                    dst_sock.setsockopt(socket.IPPROTO_IP, socket.IP_MULTICAST_IF, socket.inet_aton(dst_adapter_ip))
                    dst_sock.settimeout(timeout)
                    dst_sock.sendto(request_data,(multicast_group,port))
                    while True:
                        try:
                            response_data, server_addr = dst_sock.recvfrom(4096)
                            src_sock.sendto(response_data,client_addr)

                        except Exception:
                            break
        return

if __name__ == '__main__':
    main()

上記を /usr/local/sbin/proxy_dlna.py として保存しておきます。

保存が完了したら下記のように systemd のサービスを作成します。
今回は下記の内容を /etc/systemd/system/proxy_dlna.service として作成しました。

[Unit]
Description=Launch DLNA Proxy (/usr/local/sbin/proxy_dlna.py)
# After=wg-quick@wg0.service  #WireGuard を使っている場合はこうしておくと WireGuard が起動した後に起動するようになります。

[Service]
Type=oneshot
ExecStart=/usr/local/sbin/proxy_dlna.py

[Install]
WantedBy=multi-user.target

保存が完了しましたら systemctl enable proxy_dlna 等で有効にすれば完了です。


手順は以上となります。

ただ上記のようなプロキシで全て動作するかというとそうではなく、特に DTCP-IP 等を用いるシステムの場合は「TTL は 3ホップ以下」「RTT は 7ms 以下」という制約が存在するため実現するのが大変難しい状況になっています。

もしそのような場合については NTT の NGN 網の特殊性を用いて回避しているケースや、L2TPv3 などを上手く組み合わせて回避しているケースがネット上にいくつか転がっていますのでそちらを用いるのが良いかなと思われます。

とはいえ、当方環境ではこれで VPN ライフがかなり改善されましたので、もしよろしければ参考にしていただけたらと!

Python で “error: Unable to find vcvarsall.bat” が出た場合。

どうもみむらです。

今回は Windows で Python 使ってて、
setup.py install とかしたときに、 “error: Unable to find vcvarsall.bat” とか出た場合について。

下記のウェブサイトにて記述を見つけたのですが、一応自分向けのメモとして書き直し。
街角のリブロガー pythonでvcvarsall.batエラーが出る。



1.使っている Python のビルドされた環境を確かめる。

起動直後に現れる “MSC v.1500” というような表記からバージョンを判定しても良いのですが、
( Visual Studio を使っている人なら分かるかも・・? )

次のコードを実行して判定します。

from distutils.msvccompiler import *
get_build_version()

出力結果に “9.0” や “10.0” と出るかと思います。

 

2.レジストリに値を書き込む

値を次の場所に書き込みます:

キー名: HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\VisualStudio\(Python の MSVC のバージョン)\Setup\VC

値名:ProductDir

値:(現在使っている Visual Studio の VC フォルダへのパス)

 

例として、私の環境で使っている Python 2.7 が MSVC 9.0 環境でビルドされており、

一方で私自身は Visual Studio 2013 を使っています。

この場合値は次のようになります:

キー名: HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\VisualStudio\9.0\Setup\VC

値名:ProductDir

値:C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC

image

 

また、1で 10.0 と出た場合は、

HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\VisualStudio\10.0\Setup\VC に。

その他 x86 (32bit) 環境の場合は、

HKEY_LOCAL_MACHINE\SOFTWARE\Microsoft\VisualStudio\9.0\Setup\VC になって、

パスも C:\Program Files\Microsoft Visual Studio 12.0\VC

等になるかと思います。

・・取り急ぎ、メモ。

Windows で easy_install と pip を使えるようにする

みむらです。ちょっとメモ。

 


1.事前に PATH に C:\Python27 と C:\Python27\Scripts を通しておく

私の環境だと、Python 2 系と Python 3系が入っていますが、
どっちかに通しておけば、 “py” で 2系、 “py –3” で 3系 を起動できるので良い感じ。

“python” を実行したときに 2 を使う人か 3 を使う人かでどっちかに通しておけば良いかなと。

・・私は 2 系の方をまだ使うので 2.7 に通しました。 

 

ちなみに PATH を通すには:

1.「システムとセキュリティ」を選択
image

 

2.「システム」を選択

image

 

3.「システムの詳細設定」を選択

image

 

4.「詳細設定」タブ内の「環境変数」を選択

image

 

5.Path を追記する

“PATH” という項目が無い場合は、「新規」をクリック

image

 

”PATH” という項目がある場合は、その項目をダブルクリック

image

 

でもって、こんな感じで設定する。

image

すでに項目がある場合は、そのパスの後ろに “;” 書いてパスを追記する。

複数の場所に PATH を設定したい場合は、”;” で区切って書いていく。

 


2.easy_install をインストールする。

https://bootstrap.pypa.io/ez_setup.py

ここから ez_setup.py をダウンロードしてきて、ダブルクリックで実行
勝手にファイルをダウンロードしてインストールしてくれる。

・・もし、ダブルクリックしても動かない場合は
コマンドプロンプトを開いて、

python ez_setup.py

と打ち込むと動きます。

 

また、コマンドプロンプトを開いてそこまで移動するのが面倒・・という場合は、

Shift キーを押しながら右クリックをしますと、下図のような項目が出ますので、

わざわざ移動せずともコマンドプロンプトを指定したフォルダをカレントディレクトリとして起動することが出来ます。

image

image

 


3.pip をインストール

コマンドプロンプトを開いて、

easy_install pip

を実行すると導入されます。


後は PATH を通してありますので、

image

こんな感じで、いきなり “pip” と入力しても動くようになります。

 

・・ひとまず、参考まで。

Python の内包表現と演算子と。

みむらです。

最近ちょっと Python を触ってみようかとということで、
しばらくぶりに触っています。

ってことで忘れそうなことのメモ。

・・・Python をよく使っている人からするとぶん殴られそうな気がしますが、
とりあえず、勉強ということで躓いたところをメモしていきます。

 

内包表現:

[追加要素 for 変数 in リスト if 条件]

使い方としては、

l = ((0,1),(2,3),(4,5))
m = [i for (i,j) in l]
>> (0,2,4)

みたいな。

なんとなく、C# だと、

var l = new[] { new[] { 0, 1 }, new[] { 2, 3 }, new[] { 4, 5 } };
var m = l.Select(i => i[0]);

みたいな感じ・・ですかね。

あとは条件式を加えて、

[i for (i,j) in l if i > 0]

みたいなやつに関しては

var l = new[] { new[] { 0, 1 }, new[] { 2, 3 }, new[] { 4, 5 } };
var m = l.Where(i => i[0] > 0).Select(i => i[0]);

みたいな感じ。

 


演算子のメモ:

割り算において “/” と “//” 演算子があって、

3.0/2.0 = 1.5

3.0//2.0 = 1.0

というように、”//” 演算子だと結果が整数に丸められる。

丸め方としては、

math.floor で丸めた時と同じ動作となるっぽい。

1.5 → 1.0

-1.5 → -2.0

考え方としては「負の無限大」方向に丸める。

データとして考えるなら、 0x0 の方向に丸める、と考えると良いのかもしれない。

(負数は2の補数表現で表現されることを考えてみると。)

逆に、 “0” 方向に丸める場合は、 int で括ると良い感じ。

ex. int(1.5 / 1.0)

1.5 → 1.0

-1.5 → –1.0

あとは round 関数で丸める方法なんてのもありますが、

これは昔書いた「銀行丸めと四捨五入。」を参照してください。

 

それと、 “**” 演算子が存在し、

これは C# で言うところの Math.Pow (べき乗計算)に該当。

ex.)

2**10 = 1024


要素数を数える:

私自身、ちょっとはまったのですが・・(汗

C++ だと hoge.size() とか書いて、

C# だと hoge.Length だったり、動的に求める必要がある物については hoge.Length() だったり。

Python の場合は、 len(hoge) で求まる。

また、 hoge.count() と叩くと「引数が足りない!」と怒られるんですが、

これは C# の hoge.Count(1) と同じで、要素内に指定されたものがいくつ入っているかを返すメソッド。

ただし、C# の hoge.Count() のように、引数なしだと要素数が返ってくるということはなく、

「引数が足らない!」としかられるだけなので注意。

Python + Django してみた。 MTV Model について

どうも書いてて、 MVC っぽいようで、MVC っぽくないので調べてみると、

FAQ 全般 — Django v1_0 documentation
http://djangoproject.jp/doc/ja/1.0/faq/general.html

ここいはく、 Model – Template – View ということで、 MTV モデルというんだそうな。

MVC モデルでは、

Model : データとデータに対する手続きを記述。
View : Model のデータをユーザが見られるように表示する。UI への出力。
Controller : ユーザの入力に対して応答する。処理が必要なら Model に依頼し、表示が必要なら View に依頼する。

http://ja.wikipedia.org/wiki/Model_View_Controller

という感じなのですが、

Django を MVC として見ようとすると、
個人的に View のロジックと表現技法が分離されていない印象をうけて、
Controller もDjango 全体をさすようにに見えて、どうも本来扱う部分を超えてしまう。

というように私には見えてしまってしょうがない感じだったのです。

 

そこで、
最初に示したページにある、 MTV モデルに合わせて考え直してみると、かなりしっくり来て、

Model : Database に格納してあるデータ、および付随する処理(models.Model より提供)
Template : テンプレートファイルによってデザインされた各ページのデザイン
View : リクエストがあった URI ごとに、どのページを見せるかを記述する処理。

ということで、Django を MVC として見て、脳内に疑問符が浮かんだ方は、
公式ウェブサイトで提唱されている、MTV モデル、つまり、
View を Template そして Controller を View とすることで、
Django の設計思想がモデルとしてばしっと来るのではないでしょうか。

Private