以下の内容はhttps://turgenev.hatenablog.com/entry/2025/04/24/022327より取得しました。


LinuxでCisco VPN (Anyconnect)を別のNetwork Namespaceで動かす

今回はCisco VPNに関する記事です。

Cisco VPNを普通に使う場合、全通信がVPN経由になってしまうという問題があります。VPNでないとアクセスできないリソースにアクセスするとき以外はVPNを使わないほうが快適に通信できます。

とはいえLinuxであれば、nftables(iptables)とかip ruleとかをいじることで割と何とでもなってしまう部分はあるのですが、Cisco VPNがルールを追加するたびにルールを挿入する必要がある(接続開始時だけならまだしも途中で再接続になって追加される場合もあるので自動化したくなる)、複雑なネットワーク構成にしているとCisco VPNがつながりづらいまたはつながらなくなる(例えばPPPoEを有効にしているとダメだった)、といった問題があります。

これらを回避するため、Cisco VPNをメイン環境と隔離された場所で動作させたくなります。

隔離の方法は、最も重くて大がかりなVMから、DockerやLXCのようなコンテナ、chrootのようなかなり薄いものまでいろいろありますが、ネットワークだけに焦点を当てた(かなり薄い)隔離の方法としてLinuxにはNetwork Namespace(ネットワーク名前空間netns)があります。これを使ってCisco VPNを動かしてみました。

ちなみにcui(/opt/cisco/secureclient/bin/vpn)だけで動かすならもっと楽にできると思うのですが、自分のVPNサーバーの場合はGUIじゃないと動かない方式だったので、少し面倒です。ただし、openconnect-ssoというオープンソース実装を使う方法もあり、これは最後に載せています。

試した環境はLinux Mint 22.1 MATE(Ubuntu 24.04ベース)です。

Network Namespaceの準備

では、メイン側を10.183.0.2、Cisco側を10.183.0.1としてnetnsを作成・設定していきます。まずnetnsと仮想ケーブルを作成します。以下、基本的にはrootが必要です。

ip netns add cisco
ip link add cisco0 type veth peer name cisco1 netns cisco

つぎにメイン側でIPなどを設定します。

ip addr add 10.183.0.2/24 dev cisco0
ip link set cisco0 up

cisco側でも設定します。ip netns exec cisco bashとかでシェルに入ります。こっちではloもupする必要があります。

ip addr add 10.183.0.1/24 dev cisco1
ip link set cisco1 up
ip link set lo up
ip route add default via 10.183.0.2 dev cisco1

さらにメイン側では必要に応じて、IP転送の有効化、MASQUERADEルールの追加、ファイアウォールで10.183.0.1を許可、などをしておきます。

これで8.8.8.8へのpingとか1.1.1.1へのcurlとかが通るようになるはずです。通らなかったらtcpdumpとかで確認しましょう(以降同様)。

しかしcisco側ではsystemd-resolvedが動いていないのでドメイン解決ができません。

そこでsocatを使って無理やりメイン側の127.0.0.53をcisco側の127.0.0.53:53で見えるようにします。

socat -4 UDP-RECVFROM:53,bind=10.183.0.2,reuseaddr,fork UDP-SENDTO:127.0.0.53:53
ip netns exec cisco socat -4 UDP-RECVFROM:53,bind=127.0.0.53,reuseaddr,fork UDP-SENDTO:10.183.0.2:53

これでgoogle.comへのpingとかcurlが通るようになるはずです。

ちなみにドメイン解決をするためのベストプラクティスはあんまり良く分かっていません。socatよりは/etc/netns(今回だったら/etc/netns/cisco/resolv.conf)を使う方法のほうがマトモそうですが、メイン側の/etc/resolv.confに更新が入ると動かなくなるという問題があります。またiptablesのポート転送だとloに向かってるのをcisco1に向けなきゃいけなかったりメイン側でroute_localnetをしなきゃいけなかったりしそうで面倒そうです。Cisco VPNを動かすにあたってはこの部分のパフォーマンスはそこまで重要ではないだろうということでsocatにしました。

cgroupの設定

後ほど使うので、ここでcgroup関連の設定をします。

過去記事のcgroupのnet_clsを使って、特定のプロセス(ツリー)に対するiptables/nftablesルールを設定する - turgenev’s blogに従って、以下のようにします。(パッケージがなければ適宜入れてください)

cgroupfs-mount
mkdir /sys/fs/cgroup/net_cls/cisco
echo 183 > /sys/fs/cgroup/net_cls/cisco/net_cls.classid

とします。(echoのところの183は何でもいいです)

cgexec -g net_cls:cisco echo test

などとしてちゃんと動作するか確認しておきます。

Cisco VPNの基本設定

Cisco VPNをインストールします。

自分のところのVPNサーバーから提供されるバージョン(5.1.3.62)では、Ubuntu 24.04だと依存ライブラリが新しいものしか入っていないため動作しないという問題があったので、これとかに従ってUbuntu 22.04のレポジトリからパッケージを入れました。ちなみにこういうときのCiscoの詳細なエラーログはsyslogに書かれています。新しいバージョンを指す古い名前のシンボリックリンクを勝手に作るのもやってみましたが動きませんでした。

Cisco VPNでは、実際にVPN接続を担当する部分であるvpnagentdというsystemdのサービスが動作しています。そこで、このvpnagentdを、さっきの「cisco」というnetnsで動作するように変える必要があります。

sudo systemctl edit vpnagentd.service

とすると、この行とこの行の間に設定を追加してくださいみたいなエディタ画面が出てくるので、以下のように、今まで設定したnetnsとcgroupを使って起動するような設定を入れます。

[Service]
ExecStart=
ExecStart=bash -c "cgroupfs-mount; cgexec -g net_cls:cisco /opt/cisco/secureclient/bin/vpnagentd -execv_instance"
NetworkNamespacePath=/run/netns/cisco

ip netns execをベタ書きしてもいいですが、せっかくNetworkNamespacePathというのがあるのでこれを使います。ちゃんと理解していないのですが、新規でNetwork Namespaceに入るとその内部ではcgroupfs-mountが行われていない状態になっているようなので、わざわざbashで実行しています。もっといいやり方があるかもしれません。

完了したら、vpnagentdをrestartしておきます。

sudo systemctl restart vpnagentd.service

vpnagentdをポート転送

この状態でCisco VPNGUIを普通に起動すると、「VPNサービスへの接続がありません」みたいな表示が出ます。これはvpnagentdが別のNetwork Namespaceで起動しているのでメイン側からだと見えないからです。

tcpdumpでパケットを見てみると、どうやらvpnagentdとの通信には127.0.0.1:29754が使われているらしいということがわかります。そこでこれもsocatで無理やり転送します。

socat TCP-LISTEN:29754,bind=127.0.0.1,reuseaddr,fork TCP:10.183.0.1:29754
sudo ip netns exec cisco socat TCP-LISTEN:29754,bind=10.183.0.1,reuseaddr,fork TCP:127.0.0.1:29754

これで先ほどのエラーは出なくなり、接続が開始できる状態になります。

vpnagentdに透過プロキシを適用

接続しようとしてみると、VPNサーバーへの認証が通るところまではいけるのですが、その後接続が開始せず、1分くらい待ってタイムアウトしてしまいます。

syslogにはこんな感じのエラーが出ています。

2025-04-06T18:38:52.966632+09:00 MY-MACHINE csc_vpndownloader_major[811322]: Cisco Secure Client - Downloader (Second - Major VPN) started, version 5.1.3.62
2025-04-06T18:38:52.966701+09:00 MY-MACHINE csc_vpndownloader_major[811322]: Function: connectTransport File: ../../vpn/Common/IPC/SocketTransport.cpp Line: 1192 Invoked Function: ::connect Return Code: 111 (0x0000006F) Description: unknown
2025-04-06T18:38:52.966768+09:00 MY-MACHINE csc_vpndownloader_major[811322]: Function: Connect2Peer File: ../../vpn/Common/IPC/IpcP2pConnection.cpp Line: 249 Invoked Function: CSocketTransport::connectTransport Return Code: -31588340 (0xFE1E000C) Description: SOCKETTRANSPORT_ERROR_CONNECT
2025-04-06T18:38:52.966826+09:00 MY-MACHINE csc_vpndownloader_major[811322]: Function: ConnectorEndpoint File: ../../vpn/Common/IPC/IpcP2pEndpointFactory.cpp Line: 95 Invoked Function: CIpcP2pConnection::InitiateConnector Return Code: -31588340 (0xFE1E000C) Description: SOCKETTRANSPORT_ERROR_CONNECT
2025-04-06T18:38:52.966894+09:00 MY-MACHINE csc_vpndownloader_major[811322]: Function: initSecondInstance File: ../../../vpn/Downloader/Downloader.cpp Line: 698 Invoked Function: CIpcP2pEndpointFactory::ConnectorEndpoint Return Code: -31588340 (0xFE1E000C) Description: SOCKETTRANSPORT_ERROR_CONNECT
2025-04-06T18:38:52.966970+09:00 MY-MACHINE csc_vpndownloader_major[811322]: Function: init File: ../../../vpn/Downloader/Downloader.cpp Line: 452 Invoked Function: CDownloader::initSecondInstance Return Code: -31588340 (0xFE1E000C) Description: SOCKETTRANSPORT_ERROR_CONNECT
2025-04-06T18:38:52.967045+09:00 MY-MACHINE csc_vpndownloader_major[811322]: Function: terminateIpcConnection File: ../../vpn/Common/IPC/IPCTransport.cpp Line: 470 Closing IPC connection with '0'ms timeout for peer closure
2025-04-06T18:38:52.967124+09:00 MY-MACHINE csc_vpndownloader_major[811322]: Function: Main File: ../../../vpn/Downloader/Downloader.cpp Line: 728 Invoked Function: CDownloader::Init Return Code: -31588340 (0xFE1E000C) Description: SOCKETTRANSPORT_ERROR_CONNECT
2025-04-06T18:38:52.967194+09:00 MY-MACHINE csc_vpndownloader_major[811322]: Function: main File: ../../../vpn/Downloader/Downloader.cpp Line: 807 Invoked Function: CDownloader::Main Return Code: -31588340 (0xFE1E000C) Description: SOCKETTRANSPORT_ERROR_CONNECT
2025-04-06T18:38:52.967266+09:00 MY-MACHINE csc_vpndownloader_major[811322]: Cisco Secure Client - Downloader (Second - Major VPN) exiting, version 5.1.3.62 , return code 1 [0x00000001]
(1分経過)
2025-04-06T18:39:52.958843+09:00 MY-MACHINE csc_vpndownloader_minor[811307]: Function: WaitForConnection File: ../../vpn/Common/IPC/IpcP2pEndpointFactory.cpp Line: 177 Invoked Function: CIpcP2pAcceptor::WaitForConnection Return Code: -16973811 (0xFEFD000D) Description: IPCP2PACCEPTOR_ERROR_CONNECT_TIMEOUT
2025-04-06T18:39:52.959168+09:00 MY-MACHINE csc_vpndownloader_minor[811307]: Function: initFirstInstance File: ../../../vpn/Downloader/Downloader.cpp Line: 624 Invoked Function: CIpcP2pEndpointFactory::WaitForConnection Return Code: -16973811 (0xFEFD000D) Description: IPCP2PACCEPTOR_ERROR_CONNECT_TIMEOUT
2025-04-06T18:39:52.959290+09:00 MY-MACHINE csc_vpndownloader_minor[811307]: Function: init File: ../../../vpn/Downloader/Downloader.cpp Line: 443 Invoked Function: CDownloader::initFirstInstance Return Code: -16973811 (0xFEFD000D) Description: IPCP2PACCEPTOR_ERROR_CONNECT_TIMEOUT
2025-04-06T18:39:52.959413+09:00 MY-MACHINE csc_vpndownloader_minor[811307]: Function: terminateIpcConnection File: ../../vpn/Common/IPC/IPCTransport.cpp Line: 470 Closing IPC connection with '0'ms timeout for peer closure
2025-04-06T18:39:52.959555+09:00 MY-MACHINE csc_vpndownloader_minor[811307]: Function: Main File: ../../../vpn/Downloader/Downloader.cpp Line: 728 Invoked Function: CDownloader::Init Return Code: -16973811 (0xFEFD000D) Description: IPCP2PACCEPTOR_ERROR_CONNECT_TIMEOUT
2025-04-06T18:39:52.959689+09:00 MY-MACHINE csc_vpndownloader_minor[811307]: Function: main File: ../../../vpn/Downloader/Downloader.cpp Line: 807 Invoked Function: CDownloader::Main Return Code: -16973811 (0xFEFD000D) Description: IPCP2PACCEPTOR_ERROR_CONNECT_TIMEOUT
2025-04-06T18:39:52.959803+09:00 MY-MACHINE csc_vpndownloader_minor[811307]: Cisco Secure Client - Downloader (First - Minor VPN) exiting, version 5.1.3.62 , return code 1 [0x00000001]
2025-04-06T18:39:52.965137+09:00 MY-MACHINE csc_ui[687394]: Function: launchCachedDownloader File: ../../vpn/Api/ConnectMgr.cpp Line: 8526 Invoked Function: ConnectMgr::launchCachedDownloader Return Code: 1 (0x00000001) Description: Cached Downloader terminated abnormally

cisco側でtcpdumpをしてみると、こんどはcisco側(vpnagentdの子プロセス)からメイン側のlocalhostのポートにむかって接続しようとしてTCP resetが返ってきてしまっているのがわかります。

このポートはエフェメラルポート範囲で、接続のたびに番号が変わってしまうようなので、socatのように固定のポート転送をすることはできません。

そこで透過プロキシを使うことにします。過去記事の透過プロキシを用いて特定アプリケーションのTCP・UDP通信をSOCKS5経由にする方法(Windows・Linux(iptables TPROXY)・Androidなど) - turgenev’s blogに従って以下の設定をします。

  • メイン側(例えば10.183.0.2:1830)にSOCKS5サーバー(3proxyなど)を建てる。
  • cisco側(例えば127.0.0.1:22222)では上記SOCKS5サーバーを使う透過プロキシ(redsocksあるいはhev-socks5-tproxyなど)セットアップする。
    • UDPは使わないので不要。

そして、cisco側で以下のように設定します。ここでようやくcgroupを使います。これによりvpnagentd(およびその子プロセス)だけにルールが適用されます。

iptables -t nat -A OUTPUT -p tcp ! --dport 29754 -s 127.0.0.1 -m cgroup --cgroup 183 -j REDIRECT --to-ports 22222

今回はTCPだけなのでTPROXYではなくREDIRECTターゲットで十分です。また、localhost関連かつ29754番宛(vpnagentdとの通信)ではないものだけに限定しています(この限定がなくても動きはしますが)。

これでもう一度Cisco VPNでの接続を試すと、こんどはうまくいくはずです。これで完成です。

メイン側のDNSへの影響

前述の/etc/netns使えなくなる問題のため、今回の方法では/etc/resolv.confを両方のnetnsで共有しています。Cisco VPNが有効になると、このresolv.confが書き換えられてしまい、メイン側での名前解決が上手く動作しなくなります(最終的には127.0.0.53を試してくれるが、数秒ほど遅延する)。

対策としては、あまり綺麗ではないのですが、Cisco VPNが設定してくるDNSサーバーのIPのUDP53番へのOUTPUT通信をiptables(nftables)でREJECTすることで、ほぼタイムラグなく127.0.0.53が使われるようになります。iptablesならこんな感じです。

sudo iptables -A OUTPUT0 -p UDP -d 10.20.30.40 --dport 53 -j REJECT --reject-with icmp-host-unreachable

port unreachableではなくhost unreachableにしたらこのIP使うの諦めてくれないかなあと思ってやってみたのですが、あまり差はなさそうです。

別解: openconnect-ssoを使う

「自分のVPNサーバーの場合はGUIじゃないと動かない方式だった」と最初で書きましたが、Anyconnectと互換性のあるオープンソース実装であるopenconnect(apt installで入れられる)と、そこにSSOログイン機能を付加するopenconnect-ssoというのがあり、これを使うとCUIベースで接続することができます。

ブラウザ認証画面がQt xcbが何たらというエラーでうまく開かなかったので、このissuesに従ってQT_QPA_PLATFORM="vnc:port=5901"などと設定したところ、VNC経由で認証することができました。

ところで、公式クライアントは~/.ciscoに認証時のcookieを保存してくれるので毎回認証しなくていいのですが、こちらの方法だと毎回入れなければいけません。openconnect自体には--cookie-on-stdinというオプションがあったりするので多分何か方法があると思うのですが、情報があまりなさそうなのとこの記事の(公式クライアントを使う)方法がうまくいったので一旦諦めました。

別解2: LXC

最初でも触れましたが、netnsより重くVMよりは軽い分離方式としてLXCがあります。linux mintのvirginiaで試してみましたが、Cisco VPNは普通に動きました。ただしTailscale in LXC containers · Tailscale Docsなどにある通り、/dev/net/tunがコンテナ側から使えるようにしておく必要があります。

LXCでGUIを動かす方法についてはLXCの非特権(rootless)コンテナでLinux MintのGUI(MATE)をそれっぽく動かす - turgenev’s blogも参照してください。

余談: ヘッドレスマシンでの使用

今回はGUIでしか動作しないCisco VPNの機能を使うことにしていました。物理ディスプレイがなくヘッドレスで運用しているマシンでGUIを使う方法はいくつかありますが、xrdpだとリモートユーザーという扱いになってしまいCisco VPNが動きません。

従って、より物理ディスプレイに近い環境にするためVNCを使いました。

loginctlと打つとすでにc2という「SEAT」部分が「-」になっていないセッションがあり、loginctl show-session c2で見てみるとこのDisplay=:0と表示されていました。従ってこの:0のディスプレイをVNCで見えるようにするため以下のコマンドを実行します。(auth部分はps aux | grep Xorgで出てくるものに対応)

sudo x11vnc -display :0 -auth /var/run/lightdm/root/:0 -forever -shared -nopw -loop

セキュリティのことはとりあえず無視しています(nopwなど)。-loopにより、デスクトップからのログアウト後に自動でx11vncが再開します。ここから操作すれば普通にCisco VPNを使うことができます。

Mateにログイン後のVNCの反応が非常に遅かった(lightdm-gtk-greeterでのログイン時は問題なし)のですが、networking - x11vnc Headless on Ubuntu is very slow until monitor connected - Ask Ubuntuの通りにMarco (no compositor)を使用するようにしたら解決しました。

 



以上の内容はhttps://turgenev.hatenablog.com/entry/2025/04/24/022327より取得しました。
このページはhttp://font.textar.tv/のウェブフォントを使用してます

不具合報告/要望等はこちらへお願いします。
モバイルやる夫Viewer Ver0.14