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:

  • itsziget/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.
  • itsziget/hosts-gen: Az IP-domain párokat generálja a jwilder/docker-gen image-et örökölve.
  • itsziget/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:

version: "2"

volumes
:
  hosts
:

services
:
  hosts-updater
:
    image
: itsziget/hosts-updater:v1.0.0
    container_name
: hosts-updater
    volumes
:
      - /etc/hosts:/hosts/orig
      - /etc/hosts.docker.tpl:/hosts/tpl:ro
      - hosts:/hosts
  hosts-gen
:
    image
: itsziget/hosts-gen:v1.0.0
    container_name
: hosts-gen
    volumes
:  
      - /var/run/docker.sock:/tmp/docker.sock:ro
      - hosts:/hosts
    environment
:
      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.

version: "2"

services
:
  remote-mysql
:
    image
: itsziget/ssh-tunnel:v1.0.0
    volumes
:
      - "${HOME}/.ssh/id_rsa.pub:/root/.ssh/id_rsa.pub"
      - "${HOME}/.ssh/id_rsa:/root/.ssh/id_rsa"
    environment
:
      VIRTUAL_HOST
: remote-mysql
      TUNNEL_HOST
: user@remotehost:2200
      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 itsziget/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.

version: "2"

services
:
  remote-web
:
    image
: itsziget/ssh-tunnel:v1.0.0
    volumes
:
      - "${HOME}/.ssh/id_rsa.pub:/root/.ssh/id_rsa.pub"
      - "${HOME}/.ssh/id_rsa:/root/.ssh/id_rsa"
    environment
:
      VIRTUAL_HOST
: first.remote.host,second.remote.host
      TUNNEL_HOST
: user@remotehost:2200
      TUNNEL_REMOTES
: |  
        first.remote.host:443
        second.remote.host:443
        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.

version: "2"

services
:
  privatemysql-ssh
:
    image
: itsziget/ssh-tunnel:v1.0.0
    volumes
:
      - "${HOME}/.ssh/id_rsa.pub:/root/.ssh/id_rsa.pub"
      - "${HOME}/.ssh/id_rsa:/root/.ssh/id_rsa"
    environment
:
      TUNNEL_HOST
: user@publichost:2200
      TUNNEL_REMOTES
: "mysql.private.lan:22"
    expose
:
      - 22
  privatemysql
:
    image
: itsziget/ssh-tunnel:v1.0.0
    volumes
:
      - "${HOME}/.ssh/id_rsa.pub:/root/.ssh/id_rsa.pub"
      - "${HOME}/.ssh/id_rsa:/root/.ssh/id_rsa"
    links
:
      - privatemysql-ssh
    environment
:
      VIRTUAL_HOST
: mysql.private.lan
      TUNNEL_HOST
: user@privatemysql-ssh:22
      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.

version: "2"

services
:
  privatesamba
:
    image
: itsziget/ssh-tunnel:v1.0.0
    volumes
:
      - "${HOME}/.ssh/id_rsa.pub:/root/.ssh/id_rsa.pub"
      - "${HOME}/.ssh/id_rsa:/root/.ssh/id_rsa"
    environment
:
      VIRTUAL_HOST
: privatesamba
      TUNNEL_REMOTES
: |  
        samba.private.lan:137
        samba.private.lan:138
        samba.private.lan:139
        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:

version: "2"

services
:
  publicserver
:
    image
: itsziget/ssh-tunnel:v1.0.0
    volumes
:
      - "${HOME}/.ssh/id_rsa.pub:/root/.ssh/id_rsa.pub"
      - "${HOME}/.ssh/id_rsa:/root/.ssh/id_rsa"
    environment
:
      TUNNEL_HOST
: user@publichost:2200
      TUNNEL_REMOTES
: "127.0.0.1:2200"
  privatemysql-ssh
:
    extends
:
      service
: publicserver
    environment
:
      TUNNEL_HOST
: user@publichost:2200
      TUNNEL_REMOTES
: "mysql.private.lan:22"
    expose
:
      - 22
  privatemysql
:
    extends
:
      service
: publicserver
    links
:
      - privatemysql-ssh
    environment
:
      VIRTUAL_HOST
: mysql.private.lan
      TUNNEL_HOST
: user@privatemysql-ssh:22
      TUNNEL_REMOTES
: "127.0.0.1:3306"
  privatesamba
:
    extends
:
      service
: publicserver
    environment
:
      VIRTUAL_HOST
: privatesamba
      TUNNEL_REMOTES
: |  
        samba.private.lan:137
        samba.private.lan:138
        samba.private.lan:139
        samba.private.lan:445
  privateweb
:
    extends
:
      service
: publicserver
    environment
:
      VIRTUAL_HOST
: web.private.lan
      TUNNEL_REMOTES
: |  
        web1.private.lan:443
        web2.private.lan:443
        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