2017年2月24日 星期五

Captive portal 研究心得,以 iptables 實作

之前略知使用 Taipei Free 或 CHT Wi-Fi時,會彈跳出認證(登入)的網頁,但卻不知其所以然,直到最近研究了 Captive portal 的東西。一開始用 Captive portal 關鍵字找了一些 RFC 文件,殊不知那些都是煙霧彈 XD 如:
經過了一陣抓封包後,簡言之,透過網路管控,當符合以下情境時,各個 OS 內的網路偵測小程式,就會彈跳視窗:
  1. 用戶連上 router 時,可以進行 DNS lookup
  2. 未通過認證的用戶,無法透過 router 進行網路連線請求,如連上 facebook.com
  3. 通過認證的用戶,透過 router 可以正常使用網路
在各個 OS 內,都有一隻小程式時時關注著網路變化,當網路狀態改變且尚未有連外環境,這時程式會自動進行網路連線偵測,概念上就是去連各個 OS 自訂的一個網頁位置,看看能不能抓到東西,且東西是否正常。若不正常時,就會判斷網路是要認證的,接著就會彈跳出認證視窗。



至於 router 上的實作,簡單的說,就是透過防火牆機制:
  1. 防火牆設計各種狀態,例如未認證,已認證的 flag (mark)
  2. 當用戶完成認證時,透過 CGI 執行防火牆指令,給予標記 (mark)
  3. 允許已標記的 client 封包通行
至於完整的實作,可以參考:
以下是同事分別安裝 wifidog 跟 NoCatSplash 取出的防火牆規則,以 wifidog 產出的:

-P INPUT ACCEPT
-P FORWARD ACCEPT
-P OUTPUT ACCEPT
-N WD_wlan0_AuthServs
-N WD_wlan0_Global
-N WD_wlan0_Internet
-N WD_wlan0_Known
-N WD_wlan0_Locked
-N WD_wlan0_Unknown
-N WD_wlan0_Validate
-A FORWARD -i wlan0 -j WD_wlan0_Internet
-A FORWARD -i eth0 -o wlan0 -m state --state RELATED,ESTABLISHED -j ACCEPT
-A FORWARD -i wlan0 -o eth0 -j ACCEPT
-A WD_wlan0_AuthServs -d SERVER_IP/32 -j ACCEPT
-A WD_wlan0_Internet -m state --state INVALID -j DROP
-A WD_wlan0_Internet -o eth0 -p tcp -m tcp --tcp-flags SYN,RST SYN -j TCPMSS --clamp-mss-to-pmtu
-A WD_wlan0_Internet -j WD_wlan0_AuthServs
-A WD_wlan0_Internet -m mark --mark 0x254 -j WD_wlan0_Locked
-A WD_wlan0_Internet -j WD_wlan0_Global
-A WD_wlan0_Internet -m mark --mark 0x1 -j WD_wlan0_Validate
-A WD_wlan0_Internet -m mark --mark 0x2 -j WD_wlan0_Known
-A WD_wlan0_Internet -j WD_wlan0_Unknown
-A WD_wlan0_Known -j ACCEPT
-A WD_wlan0_Locked -j REJECT --reject-with icmp-port-unreachable
-A WD_wlan0_Unknown -p udp -m udp --dport 53 -j ACCEPT
-A WD_wlan0_Unknown -p tcp -m tcp --dport 53 -j ACCEPT
-A WD_wlan0_Unknown -p udp -m udp --dport 67 -j ACCEPT
-A WD_wlan0_Unknown -p tcp -m tcp --dport 67 -j ACCEPT
-A WD_wlan0_Unknown -j REJECT --reject-with icmp-port-unreachable
-A WD_wlan0_Validate -j ACCEPT


以 NoCatSplash 產出的 iptables 為筆記:

$ iptables -S
-P INPUT ACCEPT
-P FORWARD ACCEPT
-P OUTPUT ACCEPT
-N NoCat
-N NoCat_Inbound
-N NoCat_Ports
-A FORWARD -j NoCat
-A NoCat -j NoCat_Ports
-A NoCat -j NoCat_Inbound
-A NoCat -s SERVER_IP/24 -i wlan0 -m mark --mark 0x1 -j ACCEPT
-A NoCat -s SERVER_IP/24 -i wlan0 -m mark --mark 0x2 -j ACCEPT
-A NoCat -s SERVER_IP/24 -i wlan0 -m mark --mark 0x3 -j ACCEPT
-A NoCat -j DROP
-A NoCat_Ports -i wlan0 -p tcp -m tcp --dport 25 -m mark --mark 0x3 -j DROP
-A NoCat_Ports -i wlan0 -p udp -m udp --dport 25 -m mark --mark 0x3 -j DROP
$ iptables -t nat -S
-P PREROUTING ACCEPT
-P INPUT ACCEPT
-P OUTPUT ACCEPT
-P POSTROUTING ACCEPT
-N NoCat_Capture
-N NoCat_NAT
-A PREROUTING -j NoCat_Capture
-A POSTROUTING -j NoCat_NAT
-A NoCat_Capture -p tcp -m mark --mark 0x4 -m tcp --dport 80 -j REDIRECT --to-ports 5280
-A NoCat_Capture -p tcp -m mark --mark 0x4 -m tcp --dport 443 -j REDIRECT --to-ports 5280
-A NoCat_NAT -s SERVER_IP/24 -o eth0 -m mark --mark 0x1 -j MASQUERADE
-A NoCat_NAT -s SERVER_IP/24 -o eth0 -m mark --mark 0x2 -j MASQUERADE
-A NoCat_NAT -s SERVER_IP/24 -o eth0 -m mark --mark 0x3 -j MASQUERADE


其他 iptabes 簡介:

2017年2月7日 星期二

[Linux] 使用 xargs 執行任務,參數不只接在指令尾部

原先很習慣用 find 找到東西後,搭配 -exec 去執行任務,這時要串起來的指令很方便。然而,若是要在一個清單內,找到東西後,再接一串指令時,就會試試 xargs 這指令。只是 xargs 習慣都是把變數接在指令後頭:

$ grep 我 user.dict | xargs -n 1 echo | head -n 5
分我杯羹
天知地知,你知我知
禮豈為我設
誨爾諄諄,聽我藐藐
惠子知我


此例就是找到的關鍵字”我”,再丟給 echo 去印出來。但如果,想要把找到的關鍵字都包起來時:

找到”分我杯羹”了
找到”天知地知,你知我知”了
找到”禮豈為我設”了
找到”誨爾諄諄,聽我藐藐”了
找到”惠子知我”了


這時就會卡卡的,沒錯,就派 shell 上場了:

$ grep 我 user.dict | xargs -n 1 sh -c 'echo "找到\"${@}"\"了' "${0}" | head -n 5
找到"分我杯羹"了
找到"天知地知,你知我知"了
找到"禮豈為我設"了
找到"誨爾諄諄,聽我藐藐"了
找到"惠子知我"了


如此一來,透過 sh 在包一層,就可以完美使用 xargs 做事了。而我真正的需求是檢查一個檔案清單,查看清單內的檔案是否都存在:

$ grep keyword file-list.txt | xargs -n 1 sh -c 'test -r "${@}" || echo "${@} NOT FOUND" ' "${0}"

2017年2月3日 星期五

Ansible 筆記 - 透過 Reverse SSH Tunnel 維護機器

有時候就是這樣,某些機器本身連不進去,這時要測試發佈流程時,就適合從中間機器建立 SSH Tunnel 啦。例如 A <-> B <-> C 關係中,B 可以分別連到 A 跟 C ,但 A 跟 C 彼此無法連上。這時就從 B 的角色建立個 Reverse SSH Tunnel 了

@B:

$ ssh -NR 2266:C_HOST_IP:22 user@A_HOST_IP


如此一來,在 A 機器中,就可以自連 ssh 127.0.0.1:2266 就可以直達 C 機器囉!

接著,若在 A 機器發動 ansible deploy 任務,就會有需要指定 ansible-playbook 運作時,那個 ssh port 要換一下,所幸的,用 ansible-playbook -h 就有教學啦 XD 其中 sftp 跟 scp/ssh 的參數名不一樣,所以得個別設定,完整的範例:

@A:

$ HOST=tag_Ansible_MY_Task DATA="['127.0.0.1']" ansible-playbook -i bin/echo.sh MY_Task.yml --private-key=ssh-private-key.pem --sftp-extra-args="-oPORT=2266" --scp-extra-args="-p 2266" --ssh-extra-args="-p 2266"


此例 bin/echo.sh 只是一個讓我組出 ansible-playbook 所需的格式:

$ cat bin/echo.sh
#!/bin/bash
echo "{\"$HOST\":$DATA}"
$ HOST=tag_Ansible_MY_Task DATA="['127.0.0.1']" bin/echo.sh
{"tag_Ansible_MY_Task":['127.0.0.1']}
$ head -n 7 MY_Task.yml
---
- hosts: tag_Ansible_MY_Task
  remote_user: ubuntu
  become: true
  become_user: root
  become_method: sudo

2017年1月13日 星期五

[NodeJS] 批次處理 Website snapshot 並存進 MySQL DB @ Ubuntu Server 14.04

延續之前 [NodeJS] 使用 WebShot 進行網頁截圖、顯示正確的中文(CJK)等編碼 @ Ubuntu 14.04 Server 的部分,稍微改幾行 code 就支援批次處理啦

前置環境:

$ sudo apt-get install nodejs npm xfonts-wqy xfonts-kaname
$ sudo ln -s /usr/bin/nodejs  /usr/bin/node
$ mkdir -p job/images && cd job
$ npm install webshot
程式主體:

$ vim build.js

var output_dir = 'images';
var concurrent_limit = 10;
var running_task = 0;
var total_task = [
{ domain: 'tw.yahoo.com', url: 'https://tw.yahoo.com' } ,
{ domain: 'facebook.com', url: 'https://facebook.com' } ,
];

function build_website_snapshot() {
while(total_task.length > 0 && running_task < concurrent_limit) {
var item = total_task.shift();
var url = item.url;
var domain = item.domain;

// https://github.com/brenden/node-webshot
webshot(url, output_dir+'/'+domain+'.png', {
screenSize: {
width: 320,
height: 480,
},
shotSize: {
width: 320,
height: 320,
},
timeout: 20000,
renderDelay: 3000,
userAgent: 'Mozilla/5.0 (iPhone; U; CPU iPhone OS 3_2 like Mac OS X; en-us) AppleWebKit/531.21.20 (KHTML, like Gecko) Mobile/7B298g'

}, function(err) {
if(err)
console.log(err);
running_task--;
if (running_task == 0)
console.log('done');
if (total_task.length > 0)
build_website_snapshot();
});
running_task++;
}
}


$ node build.js

如此一來,就稍微搞定批次產出了。若要把產出的東西存進 MySQL DB server,那可以再這樣做:

$ npm install mysql

$ vim import.js

var fs = require('fs');
var path = require('path');
var mysql = require('mysql');
var connection = mysql.createConnection({
  host     : 'localhost',
  user     : 'dbuser',
  password : 'dbpassword',
  database : 'dbname',
});

var scan_source_dir = 'images';
var files = [];
var sql_values = [];
fs.readdirSync(scan_source_dir).filter(function(file){
        //console.log(file);
        if (fs.statSync(path.join(scan_source_dir, file)).isFile() && file.lastIndexOf('.png') == (file.length - 4)) {
                var domain = file.substring(0, file.length - 4);
                //files[domain] = fs.readFileSync(path.join(scan_source_dir, file), {encoding: 'binary'});
                files[domain] = fs.readFileSync(path.join(scan_source_dir, file));

                sql_values.push([domain, files[domain], Math.round(new Date().getTime()/1000), Math.round(new Date().getTime()/1000)]);
        }
});
// console.log (sql_values);
/*
CREATE TABLE `snapshot_table ` (
  `id` int(11) unsigned NOT NULL AUTO_INCREMENT,
  `domain` varchar(64) NOT NULL DEFAULT '',
  `image` blob,
  `createtime` int(11) DEFAULT NULL,
  `updatetime` int(11) DEFAULT NULL,
  PRIMARY KEY (`id`),
  UNIQUE KEY `domain` (`domain`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;
*/

var sql = "INSERT INTO snapshot_table (domain, image, createtime, updatetime) VALUES ? ON DUPLICATE KEY UPDATE image=VALUES(image), updatetime=VALUES(updatetime) ";
connection.query(sql, [sql_values], function(err) {
        console.log(err);
});
connection.end();


如此一來,就可以自動掃目錄下符合 *.png 的檔案,並將 binary data 紀錄至 db server 中。

2016年12月29日 星期四

[PHP] 使用 str_getcsv 處理 CSV 格式 (Google Docs export CSV format)

這議題已經很久,之前一直沒踩到什麼,直到處理 Google doc export the CSV format ...

大部分的分析都用:

foreach ( @explode("\n", file_get_content('https://docs.google.com/spreadsheets/d/.../export?format=csv...')) as $line ) {
$field = str_getcsv($line);
}


但是,若有 enclosure 的方式(例如 "...\n....\n") 就會出包啦,這在 http://php.net/manual/en/function.str-getcsv.php 網站上也有人提到,解法就是一樣用 str_getcsv 去切 $row:

$row = str_getcsv( file_get_content('https://docs.google.com/spreadsheets/d/.../export?format=csv...'), "\n");
foreach( $row as $line ) {
$field = str_getcsv($line);
}


但是,在處理 Google Docs 又發現切不對,最後發現改用 "\r\n" 即可 :P

$row = str_getcsv( file_get_content('https://docs.google.com/spreadsheets/d/.../export?format=csv...'), "\r\n");
foreach( $row as $line ) {
$field = str_getcsv($line);
}


Updated @ 2017/05/04
可以改用 tsv 輸出: $csv = explode("\n", @file_get_contents('https://docs.google.com/spreadsheets/d/.../export?format=tsv...'));
array_shift($csv); // remove header


foreach($csv as $line) {
$fields = str_getcsv($line, "\t");
print_r($fields);
}