Skip to content

Instantly share code, notes, and snippets.

@mckelvin
Last active March 17, 2020 03:32
Show Gist options
  • Star 6 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save mckelvin/0428e05015b6fce6c7f62d47e570508b to your computer and use it in GitHub Desktop.
Save mckelvin/0428e05015b6fce6c7f62d47e570508b to your computer and use it in GitHub Desktop.
Split VPN traffic for IPSec using CHNROUTE 监听 syslog, 发现 IPSec VPN 连接建立或断开后基于CHNROUTE去修改路由表。
# 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())
<?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>
@sunmingshuai
Copy link

你好 能留个qq吗 自己搭了个l2tp vpn服务器 想做分流 能不能指导下啊

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment