Last active
March 17, 2020 03:32
-
-
Save mckelvin/0428e05015b6fce6c7f62d47e570508b to your computer and use it in GitHub Desktop.
Split VPN traffic for IPSec using CHNROUTE 监听 syslog, 发现 IPSec VPN 连接建立或断开后基于CHNROUTE去修改路由表。
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
# coding: utf-8 | |
# | |
# OS X 11 (macOS ) 之后不再支持 PPTP VPN. 但 Cisco IPSec VPN 不支持像 PPTP 的 | |
# /etc/ppp/ip-up 和 /etc/ppp/ip-down 一样方便j的机制来更新 chnroute 路由表。 | |
# 这个脚本尝试在 Cisco IPSec VPN 下自动处理 chnroute, 做的主要工作是监听 syslog, | |
# 发现 IPSec VPN 连接建立或断开后去修改路由表。 | |
# | |
# NOTE: 使用前可能需要修改 CUSTOMED_ROUTE_DATA, 建议将其设为 | |
# 排除VPN子网后的 rfc1918 定义的内网IP段 | |
# | |
# 使用方法:CUSTOMED_ROUTE_DATA 和 local.chnroute-for-ipsec.plist 中的路径 | |
# 0. 修改 | |
# 1. 将 local.chnroute-for-ipsec.plist 复制到 /Library/LaunchDaemons/local.chnroute-for-ipsec.plist | |
# 2. sudo launchctl load /Library/LaunchDaemons/local.chnroute-for-ipsec.plist 启动服务进程 | |
# 3. 正常使用 IPSec VPN 即可 | |
import os | |
import re | |
import sys | |
import math | |
import socket | |
import time | |
import urllib | |
import signal | |
import tempfile | |
import subprocess | |
import threading | |
import logging | |
from Queue import Queue, Empty as EmptyQueueError | |
SYSLOG_PATH = "/var/log/system.log" | |
ONE_DAY = 3600 * 24 | |
IDLE_SECONDS = 1.0 | |
CONNECTED_STR = "IPSec Phase 2 established" | |
DISCONNECTED_STR = "IPSec disconnecting from server" | |
APNIC_URL = "http://ftp.apnic.net/apnic/stats/apnic/delegated-apnic-latest" | |
TMP_DIR = "/tmp/route-for-ipsec/" | |
CUSTOMED_ROUTE_DATA = [ | |
("10.0.0.0", 8), | |
("192.168.1.0", 24), | |
# 192.168.3.0/24 is for vpn | |
("172.16.0.0", 12), | |
] | |
logger = logging.getLogger(__name__) | |
def _process_record(item): | |
if isinstance(item, str): | |
if item.startswith('apnic'): | |
unit_items = item.split('|') | |
starting_ip = unit_items[3] | |
num_ip = unit_items[4] | |
else: | |
starting_ip = item | |
num_ip = 1 | |
elif isinstance(item, (tuple, list)): | |
if len(item) == 2: | |
starting_ip, len_mask = item | |
num_ip = 1 << (32 - len_mask) | |
elif len(item) == 1: | |
starting_ip, num_ip = item[0], 1 | |
if starting_ip.strip('1234567890.'): | |
starting_ip = socket.gethostbyname(starting_ip) | |
if not isinstance(num_ip, (int, long)): | |
num_ip = int(num_ip) | |
imask = 0xffffffff ^ (num_ip-1) | |
# convert to string | |
imask = hex(imask)[2:] | |
mask = [0]*4 | |
mask[0] = imask[0:2] | |
mask[1] = imask[2:4] | |
mask[2] = imask[4:6] | |
mask[3] = imask[6:8] | |
# convert str to int | |
mask = [int(i, 16) for i in mask] | |
mask = "%d.%d.%d.%d" % tuple(mask) | |
# mask in *nix format | |
mask2 = 32-int(math.log(num_ip, 2)) | |
return (starting_ip, mask2) | |
def fetch_route_data(): | |
cache_dir = os.path.join(TMP_DIR, 'cache') | |
if not os.path.exists(cache_dir): | |
os.makedirs(cache_dir) | |
apnic_cache_path = os.path.join( | |
cache_dir, 'apnic-delegated-apnic-latest' | |
) | |
need_fetch = True | |
if os.path.exists(apnic_cache_path): | |
delta = time.time() - os.path.getmtime(apnic_cache_path) | |
if delta < 7 * ONE_DAY: | |
need_fetch = False | |
if need_fetch: | |
logger.info( | |
"Fetching data from apnic.net, " | |
"it might take a few minutes, " | |
"please wait..." | |
) | |
urllib.urlretrieve(APNIC_URL, apnic_cache_path) | |
data = open(apnic_cache_path).read() | |
cnregex = re.compile(r'apnic\|cn\|ipv4\|[0-9\.]+\|[0-9]+\|[0-9]+\|a.*', | |
re.IGNORECASE) | |
cndata = cnregex.findall(data) | |
return sorted({_process_record(item) for item in cndata}) | |
def get_routes_rank(routes): | |
gateway, ifname = routes | |
if ifname == "en4": | |
return 10 | |
if ifname == "en0": | |
return 1 | |
def get_default_route(): | |
lines = subprocess.check_output([ | |
"netstat", "-nr" | |
]).splitlines() | |
routes = [] | |
for line in lines: | |
if not line.startswith("default"): | |
continue | |
cols = line.split() | |
if len(cols) != 6: | |
# ipv6 | |
continue | |
# destination, gateway, flags, refs, use, netif_expire | |
gateway = cols[1] | |
netif_expire = cols[5] | |
routes.append((gateway, netif_expire)) | |
return max(routes, key=get_routes_rank)[0] | |
class ConnectionEnum(object): | |
DISCONNECTED = "disconnected" | |
CONNECTED = "connected" | |
class CHNRouteHandler(threading.Thread): | |
def __init__(self): | |
super(CHNRouteHandler, self).__init__() | |
self.queue = Queue() | |
self.runable = True | |
self.ev_connecting = threading.Event() | |
self.ev_disconnecting = threading.Event() | |
def on_vpn_connected(self): | |
if self.ev_connecting.is_set(): | |
logger.debug("Already connecting") | |
return | |
self.ev_connecting.set() | |
logger.info("on vpn connected") | |
self.queue.put(ConnectionEnum.CONNECTED) | |
def on_vpn_disconnected(self): | |
if self.ev_disconnecting.is_set(): | |
logger.debug("Already disconnecting") | |
return | |
self.ev_disconnecting.set() | |
logger.info("on vpn disconnected") | |
self.queue.put(ConnectionEnum.DISCONNECTED) | |
def run(self): | |
logger.info("CHNRouteHandler started") | |
self.route_switch_worker = RouteSwitchWorker( | |
self.ev_connecting, self.ev_disconnecting, | |
self.queue, | |
) | |
self.route_switch_worker.start() | |
fhandler = None | |
while self.runable: | |
if fhandler is None or fhandler.closed: | |
fhandler = open(SYSLOG_PATH, "r") | |
fhandler.seek(-1, 2) | |
line = fhandler.readline() | |
if not line: | |
time.sleep(IDLE_SECONDS) | |
continue | |
if "racoon" not in line: | |
continue | |
if CONNECTED_STR in line: | |
self.on_vpn_connected() | |
if DISCONNECTED_STR in line: | |
self.on_vpn_disconnected() | |
self.queue.put(None) | |
def stop(self): | |
self.runable = False | |
class RouteSwitchWorker(threading.Thread): | |
def __init__(self, ev_connecting, ev_disconnecting, queue): | |
super(RouteSwitchWorker, self).__init__() | |
self.ev_connecting = ev_connecting | |
self.ev_disconnecting = ev_disconnecting | |
self.queue = queue | |
self.runable = True | |
self.last_default_gateway = None | |
def run(self): | |
while self.runable: | |
try: | |
ev = self.queue.get(timeout=1) | |
except EmptyQueueError: | |
continue | |
if ev is None: | |
break | |
if ev == ConnectionEnum.CONNECTED: | |
try: | |
self.setup_chnroute() | |
finally: | |
self.ev_connecting.clear() | |
if ev == ConnectionEnum.DISCONNECTED: | |
try: | |
self.clear_chnroute() | |
finally: | |
self.ev_disconnecting.clear() | |
def setup_chnroute(self): | |
self.last_default_gateway = get_default_route() | |
self.route_data = fetch_route_data() + CUSTOMED_ROUTE_DATA | |
logger.info("Adding routes ...") | |
DEVNULL = open(os.devnull, 'w') | |
commands = [ | |
"route add {0}/{1} {2}".format( | |
ip, mask, self.last_default_gateway | |
) | |
for (ip, mask) in self.route_data | |
] | |
fhandler = tempfile.NamedTemporaryFile(prefix="route-add") | |
try: | |
fhandler.write(" &&\n".join(commands)) | |
fhandler.flush() | |
os.fsync(fhandler.file.fileno()) | |
p = subprocess.Popen( | |
"sh %s" % fhandler.name, shell=True, stdout=DEVNULL, | |
preexec_fn=os.setpgrp | |
) | |
p.wait() | |
finally: | |
fhandler.close() | |
logger.info("%d routes added" % len(self.route_data)) | |
def clear_chnroute(self): | |
if self.last_default_gateway is None: | |
return | |
logger.info("Deleting routes ...") | |
DEVNULL = open(os.devnull, 'w') | |
commands = [ | |
"route delete {0}/{1} {2}".format( | |
ip, mask, self.last_default_gateway | |
) | |
for (ip, mask) in self.route_data | |
] | |
fhandler = tempfile.NamedTemporaryFile(prefix="route-delete") | |
try: | |
fhandler.write(" &&\n".join(commands)) | |
fhandler.flush() | |
os.fsync(fhandler.file.fileno()) | |
p = subprocess.Popen( | |
"sh %s" % fhandler.name, shell=True, stdout=DEVNULL, | |
preexec_fn=os.setpgrp | |
) | |
p.wait() | |
finally: | |
fhandler.close() | |
self.last_default_gateway = None | |
logger.info("%d routes deleted" % len(self.route_data)) | |
def main(): | |
FORMAT = '%(asctime)-15s [%(levelname)s] [%(name)-9s] %(message)s' | |
logging.basicConfig( | |
level=logging.INFO, | |
format=FORMAT, | |
) | |
if os.geteuid() != 0: | |
logger.error("Please run in root.") | |
return 1 | |
chnroute_handler = CHNRouteHandler() | |
def exit_handler(signo, stack_frame): | |
logger.info("Got exit signal... wait a moment") | |
chnroute_handler.stop() | |
signal.signal(signal.SIGINT, exit_handler) | |
signal.signal(signal.SIGTERM, exit_handler) | |
chnroute_handler.start() | |
while chnroute_handler.is_alive(): | |
time.sleep(1) | |
if __name__ == '__main__': | |
sys.exit(main()) |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<?xml version="1.0" encoding="UTF-8"?> | |
<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> | |
<plist version="1.0"> | |
<dict> | |
<key>Label</key> | |
<string>local.chnroute-for-ipsec</string> | |
<key>ProgramArguments</key> | |
<array> | |
<string>/usr/local/bin/python</string> | |
<string>/Users/kelvin/tools/fuckgfw/chnroute-for-ipsec/chnroute_for_ipsec.py</string> | |
</array> | |
<key>RunAtLoad</key> | |
<true/> | |
<key>KeepAlive</key> | |
<true/> | |
<key>StandardOutPath</key> | |
<string>/Users/kelvin/tools/fuckgfw/chnroute-for-ipsec/chnroute-for-ipsec.log</string> | |
<key>StandardErrorPath</key> | |
<string>/Users/kelvin/tools/fuckgfw/chnroute-for-ipsec/chnroute-for-ipsec_err.log</string> | |
</dict> | |
</plist> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
你好 能留个qq吗 自己搭了个l2tp vpn服务器 想做分流 能不能指导下啊