それマグで!

知識はカップより、マグでゆっくり頂きます。 takuya_1stのブログ

習慣に早くから配慮した者は、 おそらく人生の実りも大きい。

OpenWrtでSSLHをしてHTTPS/TLSを443共有する

OpenWrtでSSLHをしてHTTPS/TLSを443共有する

TCP443ポートをSSLHで再利用する。OpenWrtで行う。

インストール

opkg install sslh
uci show  sslh

TLS(https)/SSHLの転送先

uci set sslh.default.ssh='10.1.1.1:22'
uci set sslh.default.tls='10.1.1.1:443'

uci set sslh.default.listen='xxx.xxx.xxx.xxx:443' ## ここ注意

uci set sslh.default.enable='1'
uci set sslh.default.transparent='1'

ここはPPPOEが変わるたび設定を変える必要がある。

LuCi(WEB管理画面)との衝突を回避

LucI がHTTPSを使う場合、0.0.0.0 とバッティングするので注意する。

luci のHTTPS0.0.0.0/0から指定ポートに振り変える

uci show uhttpd.main
uci add_list uhttpd.main.listen_https='$LAN_IP:443'
uci add_list uhttpd.main.listen_https='$VPN_IP:443'
uci get uhttpd.main.listen_https

再起動で反映。

service uhttpd restart

少々ややこしいが、SSHLの安定後なら、SSHLを使ってluci の管理画面自体もsslhで振り分けても良い。(ポートフォワーディング+APLN+TLSで可能)

443(tcp)への通信を許可

OpenWrtへの443ポート通信をAcceptする。

uci add firewall rurle
uci add_list firewall.@redirect[-1]='rule'
uci set firewall.@rule[-1].name='allow-443-sslh'
uci set firewall.@rule[-1].proto='tcp'
uci set firewall.@rule[-1].src='wan'
uci set firewall.@rule[-1].dest_port='443'
uci set firewall.@rule[-1].target='ACCEPT'

設定を確認して反映

uci show firewall
## 問題がなさそうなら
uci commit firewall

ファイアウォールの設定の代わりに、ポートフォワーディングでsslhへ転送しても良い。

透過(transparent)設定

このままでは、sslh がプロキシしているので、HTTPSSSHのログにルーターのIPが残るだけである。SNATしてるのと変わらない。 流石にこれはセキュリティ設定やログ管理で不便なので、透過にする。

透過設定は少々ややこしいが、SSLH公式にサンプルがある

透過にするには、iptablesでmarkしてルーティングテーブルを変えてあげなきゃいけない。

transparent モジュール(kmod-ipt-tproxy)を入れる

opkg install iptables-mod-tproxy 

有効にする。

iptables -t mangle -N SSLH
iptables -t mangle -A PREROUTING -p tcp -m socket --transparent -j SSLH
iptables -t mangle -A OUTPUT --protocol tcp --out-interface eth0 -m multiport --sport 443 --jump SSLH
iptables -t mangle -A SSLH --jump MARK --set-mark 0x1
iptables -t mangle -A SSLH --jump ACCEPT
ip rule add fwmark 0x1 lookup 100
ip route add local 0.0.0.0/0 dev lo table 100

接続を実験する

ssh -t my-cloud-vps.tld 'nc my-router 443'

接続ができたら問題なさそう。

sslh永続化する。

sslh のリッスンポートは毎回IPを指定しなくちゃいけないので設定を変える。

/etc/hotplug.d/iface/98-ifup-pppoe.sh

#!/usr/bin/env sh

## PPPoE接続後に、 スクリプトを実行する
## 2023-03-14
function get_current_ip(){
  . /lib/functions/network.sh
  network_flush_cache
  network_find_wan WAN_IF
  network_get_ipaddr IP_ADDR "${WAN_IF}"
  echo $IP_ADDR
}

function restart_sslh(){
  echo restart sslh
  source /etc/config/custom/change_ip.sh
  uci set sslh.default.listen=`get_current_ip`:443
  uci commit;
  /etc/init.d/sslh restart
  sslh_transparent;
}

function sslh_transparent(){

   ## down
  iptables -t mangle -D PREROUTING -p tcp -m socket --transparent -j SSLH
  iptables -t mangle -D OUTPUT --protocol tcp --out-interface eth0 -m multiport --sport 443 --jump SSLH
  iptables -t mangle -D SSLH --jump MARK --set-mark 0x1
  iptables -t mangle -D SSLH --jump ACCEPT
  iptables -t mangle -X SSLH
  ip rule del fwmark 0x1 lookup 100
  ip route del local 0.0.0.0/0 dev lo table 100
   ## up
  iptables -t mangle -N SSLH
  iptables -t mangle -A PREROUTING -p tcp -m socket --transparent -j SSLH
  iptables -t mangle -A OUTPUT --protocol tcp --out-interface eth0 -m multiport --sport 443 --jump SSLH
  iptables -t mangle -A SSLH --jump MARK --set-mark 0x1
  iptables -t mangle -A SSLH --jump ACCEPT
  ip rule add fwmark 0x1 lookup 100
  ip route add local 0.0.0.0/0 dev lo table 100

}

## main
function main(){
  TARGET_INTERFACE=myISP
  TARGET_DEVICE=eth1
  [ "$ACTION" = "ifup" -a "$INTERFACE" = "$TARGET_INTERFACE"  ] && {
    logger "iface $TARGET_INTERFACE / $TARGET_DEVICE up detected, do hotplug actions."
    sleep 1;
    logger "start hotplug actions."
    ## ホットプラグの/bin/sh実行シェルに注意
    restart_sslh &
    ##
    logger "hotplug actions is done.:";
  }
}


main

このような設定を仕込むが面倒なので。uhttp(LuCI)の管理画面の443を何処か別ポートに振り分けて、SSLHで捌いたほうが良さそう

SNIを入れたい

opkg で導入されている、sslh の起動スクリプト( /etc/init.d/sslh )がSNIなどに未対応のため、細かい設定が不可能だった。

もし、/etc/sslh.confなどを使って設定を書きたいなら、自分で起動スクリプトを作成する。

不十分なopkg添付の起動スクリプトを使うより、ちゃんと起動スクリプトを書いたほうが、あとあと楽でいい。

さっきホットプラグで仕込んだ transparent の iptables / fwmark も起動スクリプトに合体させられる。設定を管理しやすい 。

自作の起動スクリプトに切り替える。

既存の起動スクリプトを放棄する。

/etc/init.d/sslh disable
mv /etc/init.d/sslh /etc/init.d/sslh.opkg

自作の起動スクリプトに切り替える

mkdir -p /etc/config/custom/sslh
cd /etc/config/custom/sslh

必要なファイルを作る。

touch sslh-daemon.sh
touch sslh.conf
touch transparent-sslh.sh

sslh-daemon.sh

#!/bin/sh /etc/rc.common
START=95
STOP=01

function get_current_ip(){
  . /lib/functions/network.sh
  network_flush_cache
  network_find_wan WAN_IF
  network_get_ipaddr IP_ADDR "${WAN_IF}"
  echo $IP_ADDR
}


dir=/etc/config/custom/sslh
conf=$dir/sslh.conf
pidfile=`cat $conf | /bin/grep -o -E  'pidfile.+' | grep -Eo '".+"' | sed 's|"||g'`
transparent_sslh=$dir/transparent-sslh.sh
sslh=`which sslh`
current_ip=`get_current_ip`
if [ -z $sslh ]; then
  logger 'sslh daemon failed. sslh command not found.'
  exit
fi


start(){
  echo start sslh
  echo $sslh -F $conf --listen $current_ip:443
  $sslh -F $conf --listen $current_ip:443
  $transparent_sslh add
  logger 'sslh daemon started.'
}

stop() {
  echo stop sslh
  if [ -e $pidfile ] ; then
    pid=`cat $pidfile`
    kill $pid
    logger 'sslh daemon stopped.'
    $transparent_sslh del
  fi
}

transparent-sslh.sh

#!/bin/sh

function sslh_transparent_add(){
  ## up
  iptables -t mangle -N SSLH
  iptables -t mangle -A PREROUTING -p tcp -m socket --transparent -j SSLH
  iptables -t mangle -A OUTPUT --protocol tcp --out-interface eth0 -m multiport --sport 443 --jump SSLH
  iptables -t mangle -A SSLH --jump MARK --set-mark 0x1
  iptables -t mangle -A SSLH --jump ACCEPT
  ip rule add fwmark 0x1 lookup 100
  ip route add local 0.0.0.0/0 dev lo table 100

}

function sslh_transparent_del(){

   ## down
  iptables -t mangle -D PREROUTING -p tcp -m socket --transparent -j SSLH
  iptables -t mangle -D OUTPUT --protocol tcp --out-interface eth0 -m multiport --sport 443 --jump SSLH
  iptables -t mangle -D SSLH --jump MARK --set-mark 0x1
  iptables -t mangle -D SSLH --jump ACCEPT
  iptables -t mangle -X SSLH
  ip rule del fwmark 0x1 lookup 100
  ip route del local 0.0.0.0/0 dev lo table 100

}
function sslh_transparent_start(){
  sslh_transparent_del;
  sslh_transparent_add;
}

function main(){
  #  sslh_transparent_start;
  case $1 in
    "add")
      echo transparent iptables,route added.
      sslh_transparent_add;
      ;;
    "del")
      echo transparent iptables,route removed.
      sslh_transparent_del;
      ;;
    *)
      echo "usage"
      echo "     " $0 ' del'
      echo "     " $0 ' add'
  esac
}

if [[ $0 =~ 'transparent-sslh.sh' ]] ; then
  # execute する場合
  main $*
fi

sslh.conf

# ##########################
#  2023-03-14
#  takuya
# ###########################

verbose: 0;
foreground: false;
inetd: false;
numeric: false;
transparent: true;
timeout: 2;
user: "nobody";
pidfile: "/var/run/sslh.pid";
chroot: "/var/tmp";


# Listenアドレスは、設定に書かずにコマンド引数で与える
listen: ();

protocols:
(
  {
    name: "ssh";
    service: "ssh";
    host: "10.16.0.1";
    port: "22";
    fork: true;
  },
  {
    name: "tls";
    alpn_protocols: [ "h2", "http/1.1", "spdy/1", "spdy/2", "spdy/3" ];
    host: "10.16.0.1";
    port: "443";
    sni_hostnames: [
      "example.tld"
    ];
    log_level: 0;
    tfo_ok: true
  },
  {
    name: "tls";
    alpn_protocols : [ "h2", "http/1.1", "spdy/1", "spdy/2", "spdy/3" ];
    host: "10.8.0.1";
    port: "443";
    log_level: 0;
    tfo_ok: true
  },
);

接続を試す。

sslh -F /etc/config/custom/sslh/sslh.conf -f -v 5  --listen $current_ip:443 

接続ができたら、起動スクリプトをコピーして起動する

ln -s /etc/config/custom/sslh/sslh-daemon.sh /etc/init.d/sslh
/etc/init.d/sslh enable 
/etc/init.d/sslh start

L7 ルータとして

OpenWrtでsslhがHTTPS(443)のL7ルータして起動する。

これで、nginx の手前で、ホスト名を見て接続先を切り替えることができる。便利ですよね。

2023-03-15 追記

OpenWrtでsslh をtranspaernt起動していると、LANからのLAN -> PPPoEのグローバルIP -> sslh -> nginx の通信が、sslh へ戻らずに、transparent のために、直接LAN上に投げ出される。

transparent なので次の通信がおかしくなる。

LAN(192.168.1.5) -> sslh(192.168.1.1) -> nginx (192.168.1.10)

sslhが透過だと、nginx は lan のIPアドレスが見えてるので、Default GW へ戻らずに、直接 (nginx (192.168.1.10 -> 192.168.1.5)パケットを投げ返してしまう。

そこで、sslh のホスト上でSNATを引っ掛けてあげる。

## wrt側での対応例
iptables -t nat -I POSTROUTING --protocol tcp -s $LAN_IP/24 -d $HTTPS_SERVER_IP -i lo --dport 443 -j SNAT --to-source $ROUTER_IP

または、nginxホスト上でパケットが常にGWへ流れるようにする。

## または、nginx側でGWを固定する。
iptables -t mangle -N SSLHSSL
iptables -t mangle -A OUTPUT -o eth0 -p tcp -m multiport --sport 443 -j SSLHSSL
iptables -t mangle -A SSLHSSL --jump MARK --set-mark 0x1
iptables -t mangle -A SSLHSSL --jump ACCEPT
ip rule add fwmark 0x1 lookup 100
ip route add default via 192.168.1.1 table 100
ip route flush cache

透過のときは考えることが多いですね。

2023-03-15 追記2 firewall 再起動/PPPoE再起動

sslh を透過で使うと、firewall ( iptables / route ) を書き換えている。そのためLuciなどで firewall restart をすると設定が飛んでしまう。 またリッスンアドレスをPPPOEにしているので再接続時にリッスンできなくなる。

sslh 用の設定を firewall に記述して解決するが、firewall の設定がsslh に依存するのは嬉しくないので、hotplugを使って連動させる。

pppoe と連動させているだけでは足りなかった。

mkdir -p /etc/hotplug.d/firewall
touch /etc/hotplug.d/firewall/01.sslh
chmod +x /etc/hotplug.d/firewall/01.sslh

/etc/hotplug.d/firewall/01.sslh

#!/bin/bash
function sslh_firewall(){
  /etc/config/custom/sslh/transparent-sslh.sh $1
}

function main(){
  if [[ ! $HOTPLUG_TYPE == 'firewall' ]]; then
    return 0;
  fi
  if [[ $ZONE == 'wan' && $DEVICENAME='pppoe-ybb' ]];then
    case $ACTION in
      add)
        :
        echo '----add' >> /tmp/echo.$ZONE
        env >> /tmp/echo.$ZONE
        echo '----' >> /tmp/echo.$ZONE
        sslh_firewall add
        ;;
      remove)
        :
        echo '----remove' >> /tmp/echo.$ZONE
        env >> /tmp/echo.$ZONE
        echo '----' >>/tmp/echo.$ZONE
        sslh_firewall del
        ;;
    esac
  fi
}

main;

firewall の hotplugは環境変数を使って識別する。

ZONE=wan
USER=root
ACTION=add
HOTPLUG_TYPE=firewall
INTERFACE=isp
PWD=/
DEVICE=pppoe-isp

ACTION, ZONE,DEVICE で識別する。 ZONEにインターフェースが複数入ってるとZONE=lan action=addが複数回呼ばれるので注意。DEVICEまで含めて識別する。

firewall から削除されるとき、ACTION=remove で呼ばれる。firewallに追加されるときにACTION=addで呼ばれる。

INTERFACEとDEVICEが逆になってる気もするが。。。。

これで再起動後も問題なく動くはずである。