用netdata實時監測Shadowsocks流量

默認的Arch套件[cci]community/shadowsocks[/cci]本身並不帶有流量監測功能,只能自己做一個。我在自家服務器上為Shadowsocks設了幾個不同端口,分別給不同用戶使用。對每一個Shadowsocks端口獨立進行檢測,有助了解和管理各用戶的用量。

流量數據是通過iptables獲取的,經Python腳本讀取處理後輸出,再由netdata模組展示。

Shadowsocks設定

如果只給Shadowsocks一個端口,其設定[cci]/etc/shadowsocks/config.json[/cci]應為:
[cc lang="javascript"]
{
"server":"0.0.0.0",
"server_port":20000,
"password":"mypassword",
"local_address": "127.0.0.1",
"local_port":1080,
"timeout":300,
"method":"aes-256-cfb",
"fast_open": false,
"workers": 1,
"prefer_ipv6": false
}
[/cc]
其中[cci]20000[/cci]要改成你所選擇的端口號。

如果要為Shadowsocks設多個端口,[cci]/etc/shadowsocks/config.json[/cci]要改為:
[cc lang="javascript"]
{
"server":"0.0.0.0",
"port_password": {
"20000": "mypassword0",
"20001": "mypassword1",
"20002": "mypassword2"
},
"local_address": "127.0.0.1",
"local_port":1080,
"timeout":300,
"method":"aes-256-cfb",
"fast_open": false,
"workers": 1,
"prefer_ipv6": false
}
[/cc]
在root用戶下啟用Shadowsocks systemd服務:
[cc lang="bash"]$ systemctl enable --now shadowsocks-server@config[/cc]

iptables規則

要讓iptables監測並記錄某個端口的進出包裹數和字節數,在root用戶下加入以下iptables規則:
[cc lang="bash"]
$ iptables -A INPUT -p tcp -m tcp --dport 端口
$ iptables -A OUTPUT -p tcp -m tcp --sport 端口
[/cc]
要為每一個Shadowsocks所用的[cci]端口[/cci]運行以上命令。

每次重啟後,iptables規則都會被重置。為避免這種情況,建立[cci]/etc/iptables/iptables.rules[/cci],內容為:
[cc]
*filter
:INPUT ACCEPT [0:0]
:FORWARD ACCEPT [0:0]
:OUTPUT ACCEPT [0:0]
-A INPUT -p tcp -m tcp --dport 20000
-A OUTPUT -p tcp -m tcp --sport 20000
-A INPUT -p tcp -m tcp --dport 20001
-A OUTPUT -p tcp -m tcp --sport 20001
-A INPUT -p tcp -m tcp --dport 20002
-A OUTPUT -p tcp -m tcp --sport 20002
COMMIT
[/cc]
每一個Shadowsocks端口都要有對應的INPUT和OUTPUT規則。啟用iptables systemd服務(但不要運行start,否則現有的其他規則都會被清空):
[cc lang="bash"]$ systemctl enable iptables[/cc]
這樣,每次啟動時,[cci]/etc/iptables/iptables.rules[/cci]中的規則都會被自動加入到iptables裡。

Python腳本實時監測

建立[cci]/usr/bin/ssmonitor.py[/cci],將定期讀取iptables裡的包裹和字節總數,計算出目前流量後,寫入臨時文件:
[cc lang="python"]
#!/usr/bin/python

import signal
import time
import subprocess
import csv

# 收到SIGINT或SIGTERM訊號時優雅退出
class GracefulKiller:
kill_now = False
def __init__(self):
signal.signal(signal.SIGINT, self.kill_gracefully)
signal.signal(signal.SIGTERM, self.kill_gracefully)

def kill_gracefully(self,signum, frame):
self.kill_now = True

# 讀取iptables的stdout數據,寫入對應於進出流量的兩個字典{'port': (pkts, bytes)}
def parse_iptables():
timestamp = time.time()
raw_in = subprocess.run(['iptables', '-nvxw', '-L', 'INPUT'], stdout=subprocess.PIPE).stdout.decode('utf-8')
raw_out = subprocess.run(['iptables', '-nvxw', '-L', 'OUTPUT'], stdout=subprocess.PIPE).stdout.decode('utf-8')
lines_in = raw_in.split('\n')
lines_out = raw_out.split('\n')
table_in = {}
table_out = {}
for l in lines_in:
cells = l.split()
if len(cells) == 10:
port = cells[9][4:]
pkts = int(cells[0])
bytes = int(cells[1])
table_in[port] = (pkts, bytes)
for l in lines_out:
cells = l.split()
if len(cells) == 10:
port = cells[9][4:]
pkts = int(cells[0])
bytes = int(cells[1])
table_out[port] = (pkts, bytes)
if len(table_in) != len(table_out):
raise ValueError('iptables INPUT and OUTPUT incompatible.')
return timestamp, table_in, table_out

# 返回進出流量率字典{'port': (pkts_in, pkts_out, bytes_in, bytes_out)}
def calculate_rates(timestamp, table_in, table_out, timestamp_old, table_in_old, table_out_old):
rates = {}
for port in table_in:
dpkts_in = table_in[port][0] - table_in_old[port][0]
dpkts_out = table_out[port][0] - table_out_old[port][0]
dbytes_in = table_in[port][1] - table_in_old[port][1]
dbytes_out = table_out[port][1] - table_out_old[port][1]
dt = timestamp - timestamp_old
rates[port] = (dpkts_in / dt, dpkts_out / dt, dbytes_in / dt, dbytes_out / dt)
return rates

# 把流量率字典轉成csv字串
def to_text(rates):
to_write = ""
for port in rates:
rates_str = [port] + ["{:.3f}".format(r) for r in rates[port]]
to_write += ",".join(rates_str) + "\n"
return to_write

# 優雅退出
if __name__ == '__main__':
killer = GracefulKiller()
timestamp_old, table_in_old, table_out_old = parse_iptables()
while True:
time.sleep(2)

timestamp, table_in, table_out = parse_iptables()
rates = calculate_rates(timestamp, table_in, table_out, timestamp_old, table_in_old, table_out_old)
timestamp_old = timestamp
table_in_old = table_in
table_out_old = table_out
to_write = to_text(rates)
f = open('/tmp/ssmonitor.tmp', 'w')
f.write(to_write)
f.close()

if killer.kill_now:
break

exit(0)
[/cc]
將此文件設為可執行:
[cc lang="bash"]$ chmod +x /usr/bin/ssmonitor.py[/cc]
用root用戶測試腳本能否正常運行:
[cc lang="bash"]$ /usr/bin/ssmonitor.py[/cc]
看看以下文件包不包含目前流量數據:
[cc lang="bash"]$ cat /tmp/ssmonitor.tmp[/cc]

腳本要在每次啟動時運行,所以要建立一個systemd服務文件[cci]/usr/lib/systemd/system/ssmonitor.service[/cci]:
[cc]
[Unit]
Description=Monitors Shadowsocks bandwidth through iptables

[Service]
Type=simple
ExecStart=/usr/bin/ssmonitor.py

[Install]
WantedBy=multi-user.target
[/cc]
啟用服務:
[cc lang="bash"]$ systemctl enable --now ssmonitor[/cc]

Netdata模組

為了展示當前流量,現在做一個新的netdata模組。建立[cci]/usr/lib/netdata/python.d/shadowsocks.chart.py[/cci],內容為:
[cc lang="python"]
from base import SimpleService
import csv

def debug(text):
f = open('/tmp/pydebug.tmp', 'w')
f.write(text)
f.close()

ORDER = ['20000_bytes', '20001_bytes', '20002_bytes', '20000_pkts', '20001_pkts', '20002_pkts']
CHARTS = {
'20000_bytes': {
'options': ['20000 bytes', '20000 bytes', 'kB/s', '20000 bytes', 'shadowsocks.20000_bytes', 'area'],
'lines': [
['20000_bytes_in', None, 'absolute', 1, 1000],
['20000_bytes_out', None, 'absolute', 1, 1000],
]},
'20001_bytes': {
'options': ['20001 bytes', '20001 bytes', 'kB/s', '20001 bytes', 'shadowsocks.20001_bytes', 'area'],
'lines': [
['20001_bytes_in', None, 'absolute', 1, 1000],
['20001_bytes_out', None, 'absolute', 1, 1000],
]},
'20002_bytes': {
'options': ['20002 bytes', '20002 bytes', 'kB/s', '20002 bytes', 'shadowsocks.20002_bytes', 'area'],
'lines': [
['20002_bytes_in', None, 'absolute', 1, 1000],
['20002_bytes_out', None, 'absolute', 1, 1000],
]},
'20000_pkts': {
'options': ['20000 packets', '20000 packets', 'packets/s', '20000 packets', 'shadowsocks.20000_packets', 'area'],
'lines': [
['20000_pkts_in', None, 'absolute', 1, 1],
['20000_pkts_out', None, 'absolute', 1, 1],
]},
'20001_pkts': {
'options': ['20001 packets', '20001 packets', 'packets/s', '20001 packets', 'shadowsocks.20001_packets', 'area'],
'lines': [
['20001_pkts_in', None, 'absolute', 1, 1],
['20001_pkts_out', None, 'absolute', 1, 1],
]},
'20002_pkts': {
'options': ['20002 packets', '20002 packets', 'packets/s', '20002 packets', 'shadowsocks.20002_packets', 'area'],
'lines': [
['20002_pkts_in', None, 'absolute', 1, 1],
['20002_pkts_out', None, 'absolute', 1, 1],
]},
}

class Service(SimpleService):
def __init__(self, configuration=None, name=None):
SimpleService.__init__(self, configuration=configuration, name=name)
self.order = ORDER
self.definitions = CHARTS

def check(self):
data = self._get_data()
if not data:
return False
return True

def _get_raw_data(self):
with open('/tmp/ssmonitor.tmp', newline='') as file:
return list(csv.reader(file))

def _get_data(self):
data = {}
try:
table = self._get_raw_data()
except:
return None
for row in table:
port = row[0]
data[port + '_pkts_in'] = float(row[1])
data[port + '_pkts_out'] = -float(row[2])
data[port + '_bytes_in'] = float(row[3])
data[port + '_bytes_out'] = -float(row[4])
return data
[/cc]
根據自己的Shadowsocks端口設定,調整[cci]ORDER[/cci]和[cci]CHARTS[/cci]。

在[cci]/etc/netdata/python.d.conf[/cci]中加入一行
[cc]shadowsocks: yes[/cc]
以啟用模組。

最後,重啟netdata服務:
[cc lang="bash"]$ systemctl restart netdata[/cc]
現在就能實時監測Shadowsocks每一個端口的流量了!

發佈留言

發佈留言必須填寫的電子郵件地址不會公開。 必填欄位標示為 *