VPN-szerű megoldás Docker és SSH tunnel segítségével

cikk borítókép

Volt már olyan, hogy VPN-t kellett használnod egy távoli, privát hálózat gépeinek eléréséhez, de közben a helyi hálózati eszközökkel is szeretted volna megtartani a kapcsolatot? Én már jártam úgy, hogy azért szakadt meg a laptop és az asztali gépem közötti RDP kapcsolat, mert be kellett lépnem otthonról egy munkahelyi gépre. Szerencsére nem a VPN az egyetlen lehetőség ilyenkor. Ott van például az SSH tunnel. Ez inkább Linuxon népszerű és egyszerű. Windowson kicsit trükkösebb. Ilyenkor viszont gyakran nem úszod meg, hogy a rendszer hosts fájlját szerkeszd azért, hogy a távoli szerver virtuális hosztjait külön-külön elérhesd. Arról nem is beszélve, ha egy távoli fájlrendszert szeretnél felcsatolni például samba-n keresztül. Ekkor a samba portok átirányítása feladja a leckét. A VPN is problémás Linuxon, ha a távoli VPN szerver nem kompatibilis egyik kliens programmal sem. Én pontosan így jártam, és azóta kerestem a megoldást, amit most megtaláltam. Ehhez írtam egy kis programot, amit most bemutatok.

A probléma

Valójában a probléma több részből áll.

  • Nyilván szükség van SSH-ra, amivel a tunnelt el lehet indítani, de nem akarom a lokális portokat átirányítani.
  • Nem akarok egy sokadik portot átirányítani egy vele nem is megegyező távoli portra, mert például több szerverem is használná a 443-as portot.
  • A fentiek miatt az egészet konténerben, Dockerrel valósítom meg, de ekkor a domaineket a konténerekre kellene irányítani, amiknek az IP címe viszont megváltozhat. Én viszont nem szeretném folyamatosan átírni a domainek IP címeit manuálisan.
  • A fenti okból következően automatikusan észlelnem kellene az új konténerek létrejöttét és törlését, de eközben meg is kellene tudnom változtatni az eredeti hosts fájlt a konténereket futtató gépen.

A megoldás folyamata

És mindenre viszonylag egyszerű választ ad a Docker, csak egy kis gondolkodás és néhány szkript kellett hozzá. Képzeld el a következő folyamatot:

  • Elindul egy konténer (hosts-gen, azaz hosts generátor), ami megkapja a docker.sock fájlt, hogy a Dockerrel tudjon kommunikálni, és amikor egy új konténer létrejön, vagy törlődik, végignézi az összes konténer IP címét és az azokhoz tartozó domaineket.
  • Elindítok egy konténert, amiben definiálom környezeti változóban, hogy milyen domaineken szeretném elérni.
  • A hosts-gen konténerben futó program észleli, hogy van egy új konténer, fájlba menti a talált IP-domain párokat, de még nem közvetlenül az eredeti hosts fájlba.
  • A domainek fájlba írása után a hosts-gen konténer a docker socketen keresztül "megbíz" egy másik konténert azzal, hogy frissítse az eredeti hosts fájlt. Mindezt egy SIGHUP küldésének segítségével.
  • A megbízott konténer (hosts-updater, azaz hosts fájl frissítő) a jelet megkapva egy, az eredeti hosts fájlból készült másolatot sablonként használva mögé fűzi a hosts-gen által fájlba írt tartalmat, majd az eredeti hosts fájlba ezt beírja annak aktuális tartalma helyett. Itt a sablonfájl fontos, mert a frissítések olyan gyorsan történhetnek egymás után, hogy egyes esetekben a hosts fájl éppen még üres, amikor annak tartalmát kiolvasná a konténer és azok után már az is maradna.
  • A már hosts fájlba került domaineket a böngészőbe beírva a megfelelő konténer a választott porton keresztül megkapja a HTTP/HTTPS kérést.
  • Az SSH tunnel miatt a kérés tovább megy egy távoli, nyilvánosan elérhető szerver felé, amihez van SSH hozzáférésem.
  • Ezen a távoli szerveren a kérés akár helyben is maradhat és az adott szervernek egy hely portján (127.0.0.1:443) fejezheti be útját, vagy bármely, erről a szerverről elérhető további gép portja felé is küldhető.

A fenti folyamatot még megbolondíthatja, amikor az átirányított port valójában egy SSH port, és ezen az átirányított porton egy újabb tunnelt indítva még több gépen keresztül küldhető tovább a forgalom mindaddig, amíg a szükséges gépekhez van SSH hozzáférésem. Ahhoz, hogy ne kérjen minden alkalommal jelszót az SSH, meg kell osztani az átirányításban résztvevő szerverekkel egy nyilvános SSH kulcsot. Mindennek megoldására három Docker image-et készítettem:

  • rimelek/ssh-tunnel: Az SSH tunnel fut a konténerben, ami az átirányítások listáját várja változóban, és azt, hogy milyen szerveren keresztül történjen a port forward.
  • rimelek/hosts-gen: Az IP-domain párokat generálja a jwilder/docker-gen image-et örökölve.
  • rimelek/hosts-updater: A hosts-gen által generált IP-domain párokat beírja a konténereket futtató gép hosts fájljába.

Példák

A példákat Docker Compose konfigurációs fájlokon mutatom be, de ezek alapján bármilyen implementáció megvalósítható.

Mindenek előtt a hosts fájlt frissítő konténereket kell elindítani. Ehhez első lépésben készíts mindenképp mentést az aktuális hosts fájlodról a biztonság kedvéért. Ez a fájl egyik konténernek se lesz átadva! Hibát még nem tapasztaltam, de azt mondják, az ördög nem alszik. Ezután egy sablon hosts fájlra is szükséged lesz. Megteszi egy másolat az eredetiből, ezt viszont fel kell csatolni a hosts-gen konténernek. Linuxon, ha a másolatot a /etc/hosts.docker.tpl néven hoztad létre, a docker-compose.yml fájl a következőképpen néz ki:

  1. version: "2"
  2.  
  3. volumes:
  4.   hosts:
  5.  
  6. services:
  7.   hosts-updater:
  8.     image: rimelek/hosts-updater:v1.0.0
  9.     container_name: hosts-updater
  10.     volumes:
  11.       - /etc/hosts:/hosts/orig
  12.       - /etc/hosts.docker.tpl:/hosts/tpl:ro
  13.       - hosts:/hosts
  14.   hosts-gen:
  15.     image: rimelek/hosts-gen:v1.0.0
  16.     container_name: hosts-gen
  17.     volumes:
  18.       - /var/run/docker.sock:/tmp/docker.sock:ro
  19.       - hosts:/hosts
  20.     environment:
  21.       UPDATER_CONTAINER: hosts-updater

A legegyszerűbb példa, amikor a lokális portot irányítod át egy távoli, de elérhető szerver lokális portjára. Például van egy webszervered egy csak localhost-ról elérhető virtuális hoszttal, vagy egy MySQL szerver, aminek nincs megnyitva a portja a külvilág felé, mert az alkalmazás is ugyanazon a gépen van.

  1. version: "2"
  2.  
  3. services:
  4.   remote-mysql:
  5.     image: rimelek/ssh-tunnel:v1.0.0
  6.     volumes:
  7.       - "${HOME}/.ssh/id_rsa.pub:/root/.ssh/id_rsa.pub"
  8.       - "${HOME}/.ssh/id_rsa:/root/.ssh/id_rsa"
  9.     environment:
  10.       VIRTUAL_HOST: remote-mysql
  11.       TUNNEL_HOST: user@remotehost:2200
  12.       TUNNEL_REMOTES: "127.0.0.1:3306"

A fenti példában a konténer 3306-os portja van átirányítva a távoli szerver lokális 3306-os portjára SSH tunnellel a 2200-as nyilvános portján keresztül. A VIRTUAL_HOST változóban megadott domainen viszont csak akkor lesz elérhető a mysql szerver, ha a már említett rimelek/hosts-gen hosts-updater image-eket is használod.

Egy másik eset, amikor egy webszervered van a távoli privát hálózatban, és szeretnél hozzáférni a saját gépedről.

  1. version: "2"
  2.  
  3. services:
  4.   remote-web:
  5.     image: rimelek/ssh-tunnel:v1.0.0
  6.     volumes:
  7.       - "${HOME}/.ssh/id_rsa.pub:/root/.ssh/id_rsa.pub"
  8.       - "${HOME}/.ssh/id_rsa:/root/.ssh/id_rsa"
  9.     environment:
  10.       VIRTUAL_HOST: first.remote.host,second.remote.host
  11.       TUNNEL_HOST: user@remotehost:2200
  12.       TUNNEL_REMOTES: |
  13.         first.remote.host:443
  14.         second.remote.host:443
  15.         second.remote.host:80

A "|" (pipe) karakter teszi lehetővé, hogy többsoros szöveget is megadhass a változóban. Ha több elérni kívánt domain is van a távoli hálózatban, azokat a TUNNEL_REMOTES változóban megadhatod soronként. Minden sor egy domaint és egy portot tartalmaz kettősponttal elválasztva.

És itt jön az fentebb felvázolt trükkösebb módja az SSH tunnel használatának. Egy konténerben egy SSH portot irányíthatsz át egy gépre a távoli hálózatban, majd egy másik konténer tetszőleges portját ezen az SSH konténeren keresztül irányíthatod egy olyan szerver helyi portjára, ami szerver egyébként nem érhető el kívülről közvetlenül.

  1. version: "2"
  2.  
  3. services:
  4.   privatemysql-ssh:
  5.     image: rimelek/ssh-tunnel:v1.0.0
  6.     volumes:
  7.       - "${HOME}/.ssh/id_rsa.pub:/root/.ssh/id_rsa.pub"
  8.       - "${HOME}/.ssh/id_rsa:/root/.ssh/id_rsa"
  9.     environment:
  10.       TUNNEL_HOST: user@publichost:2200
  11.       TUNNEL_REMOTES: "mysql.private.lan:22"
  12.     expose:
  13.       - 22
  14.   privatemysql:
  15.     image: rimelek/ssh-tunnel:v1.0.0
  16.     volumes:
  17.       - "${HOME}/.ssh/id_rsa.pub:/root/.ssh/id_rsa.pub"
  18.       - "${HOME}/.ssh/id_rsa:/root/.ssh/id_rsa"
  19.     links:
  20.       - privatemysql-ssh
  21.     environment:
  22.       VIRTUAL_HOST: mysql.private.lan
  23.       TUNNEL_HOST: user@privatemysql-ssh:22
  24.       TUNNEL_REMOTES: "127.0.0.1:3306"

Most már a mysql.private.lan domainen a 3306-os porton eléred a távoli mysql szervert, ami csak a 127.0.0.1-es IP-n érhető el.

A cikk elején említettem a samba megosztás esetén a portok problémáját. Ebben a megoldásban viszont a konténer portjai lesznek átirányítva, amiben nincsen samba szerver és semmi zavaró körülmény.

  1. version: "2"
  2.  
  3. services:
  4.   privatesamba:
  5.     image: rimelek/ssh-tunnel:v1.0.0
  6.     volumes:
  7.       - "${HOME}/.ssh/id_rsa.pub:/root/.ssh/id_rsa.pub"
  8.       - "${HOME}/.ssh/id_rsa:/root/.ssh/id_rsa"
  9.     environment:
  10.       VIRTUAL_HOST: privatesamba
  11.       TUNNEL_REMOTES: |
  12.         samba.private.lan:137
  13.         samba.private.lan:138
  14.         samba.private.lan:139
  15.         samba.private.lan:445

Ha van egy "sharedfolder" nevű megosztott mappa a samba szerveren, akkor a fájlböngészőbe beírva az alábbi címet megjelenik a távoli mappa tartalma:

  • Linuxon: smb://privatesamba/sharedfolder
  • Windowson: \\privatesamba\sharedfolder

Feltételezve, hogy több olyan szerver is van a távoli hálózatban, amit szeretnél elérni valamilyen domainen, a konténerek definíciójának öröklésével kicsit rövidítheted a konfigurációs fájlt:

  1. version: "2"
  2.  
  3. services:
  4.   publicserver:
  5.     image: rimelek/ssh-tunnel:v1.0.0
  6.     volumes:
  7.       - "${HOME}/.ssh/id_rsa.pub:/root/.ssh/id_rsa.pub"
  8.       - "${HOME}/.ssh/id_rsa:/root/.ssh/id_rsa"
  9.     environment:
  10.       TUNNEL_HOST: user@publichost:2200
  11.       TUNNEL_REMOTES: "127.0.0.1:2200"
  12.   privatemysql-ssh:
  13.     extends:
  14.       service: publicserver
  15.     environment:
  16.       TUNNEL_HOST: user@publichost:2200
  17.       TUNNEL_REMOTES: "mysql.private.lan:22"
  18.     expose:
  19.       - 22
  20.   privatemysql:
  21.     extends:
  22.       service: publicserver
  23.     links:
  24.       - privatemysql-ssh
  25.     environment:
  26.       VIRTUAL_HOST: mysql.private.lan
  27.       TUNNEL_HOST: user@privatemysql-ssh:22
  28.       TUNNEL_REMOTES: "127.0.0.1:3306"
  29.   privatesamba:
  30.     extends:
  31.       service: publicserver
  32.     environment:
  33.       VIRTUAL_HOST: privatesamba
  34.       TUNNEL_REMOTES: |
  35.         samba.private.lan:137
  36.         samba.private.lan:138
  37.         samba.private.lan:139
  38.         samba.private.lan:445
  39.   privateweb:
  40.     extends:
  41.       service: publicserver
  42.     environment:
  43.       VIRTUAL_HOST: web.private.lan
  44.       TUNNEL_REMOTES: |
  45.         web1.private.lan:443
  46.         web2.private.lan:443
  47.         web2.private.lan:80

A fenti példában az első konténer definíciója az alapja az összes többinek. Csak itt kell megadnod a felcsatolandó SSH kulcsokat és a nyilvános gép címét és az image nevét is. A többinél csak a változásokat kell definiálni.

Azt hiszem, látható, hogy ez nincs olyan messze egy VPN-től. Igaz, több dolgot kell manuálisan konfigurálnod, de cserébe ezt tetszőleges módon teheted, miközben a helyi hálózatod összes gépe és a távoli hálózat meghatározott gépei a választott portokon és domaineken elérhetők egyidőben. Ugyanakkor több, mint egy egyszerű SSH tunnel, mivel a helyi portokat érintetlenül hagyhatod.

Kategóriák: 
Megosztás/Mentés

Új hozzászólás