Freeradius + Google Authenticator + LDAP + Fortigate

ืžื” ืื ืื™ืžื•ืช ื“ื•-ื’ื•ืจืžื™ ื”ื•ื ื’ื ืจืฆื•ื™ ื•ื’ื ืขื•ืงืฆื ื™, ืื‘ืœ ืื™ืŸ ื›ืกืฃ ืœืืกื™ืžื•ื ื™ ื—ื•ืžืจื” ื•ื‘ืื•ืคืŸ ื›ืœืœื™ ื”ื ืžืฆื™ืขื™ื ืœื”ื™ืฉืืจ ื‘ืžืฆื‘ ืจื•ื— ื˜ื•ื‘.

ื”ืคืชืจื•ืŸ ื”ื–ื” ื”ื•ื ืœื ืžืฉื”ื• ืกื•ืคืจ ืžืงื•ืจื™, ืืœื ืฉื™ืœื•ื‘ ืฉืœ ืคืชืจื•ื ื•ืช ืฉื•ื ื™ื ืฉื ืžืฆืื™ื ื‘ืื™ื ื˜ืจื ื˜.

ื›ืœ ื›ืš ื ืชื•ืŸ

ะ”ะพะผะตะฝ ืฉืœ Active Directory.

ืžืฉืชืžืฉื™ ื“ื•ืžื™ื™ืŸ ืฉืขื•ื‘ื“ื™ื ื‘ืืžืฆืขื•ืช VPN, ื›ืžื• ืจื‘ื™ื ื›ื™ื•ื.

ืคื•ืขืœ ื›ืฉืขืจ VPN ืžื–ืœื–ืœ.

ืฉืžื™ืจืช ื”ืกื™ืกืžื” ืขื‘ื•ืจ ืœืงื•ื— ื”-VPN ืืกื•ืจื” ืขืœ ืคื™ ืžื“ื™ื ื™ื•ืช ื”ืื‘ื˜ื—ื”.

ืคึผื•ึนืœึดื™ื˜ึดื™ืงึธื” ืคื•ืจื˜ื™ื ื˜ ื‘ื™ื—ืก ืœืืกื™ืžื•ื ื™ื ืฉืœืš, ืืชื” ืœื ื™ื›ื•ืœ ืœืงืจื•ื ืœื–ื” ืคื—ื•ืช ืž-zhlob - ื™ืฉ ืขื“ 10 ืืกื™ืžื•ื ื™ื ื‘ื—ื™ื ื, ื”ืฉืืจ - ื‘ืžื—ื™ืจ ืžืื•ื“ ืœื ื›ืฉืจ. ืœื ืฉืงืœืชื™ RSASecureID, Duo ื•ื›ื“ื•ืžื”, ื›ื™ ืื ื™ ืจื•ืฆื” ืงื•ื“ ืคืชื•ื—.

ื“ืจื™ืฉื•ืช ืงื“ื: ืžืืจื— * nix ืขื ืžื‘ื•ืกืก freeradius, ssd - ื ื›ื ืก ืœื“ื•ืžื™ื™ืŸ, ืžืฉืชืžืฉื™ ื”ื“ื•ืžื™ื™ืŸ ื™ื›ื•ืœื™ื ืœื‘ืฆืข ืื™ืžื•ืช ื‘ื• ื‘ืงืœื•ืช.

ื—ื‘ื™ืœื•ืช ื ื•ืกืคื•ืช: ืชื™ื‘ืช ืฉืœื™ื ื”, ืคื™ื’ืœื˜, freeradius-ldap, ื’ื•ืคืŸ rebel.tlf ืžื”ืžืื’ืจ https://github.com/xero/figlet-fonts.

ื‘ื“ื•ื’ืžื” ืฉืœื™ - CentOS 7.8.

ื”ื”ื™ื’ื™ื•ืŸ ื‘ืขื‘ื•ื“ื” ืืžื•ืจ ืœื”ื™ื•ืช ื›ื“ืœืงืžืŸ: ื‘ืขืช ื—ื™ื‘ื•ืจ ืœ-VPN, ื”ืžืฉืชืžืฉ ื—ื™ื™ื‘ ืœื”ื–ื™ืŸ ื›ื ื™ืกื” ืœื“ื•ืžื™ื™ืŸ ื•-OTP ื‘ืžืงื•ื ืกื™ืกืžื”.

ื”ื’ื“ืจืช ืฉื™ืจื•ืชื™ื

ะ’ /etc/raddb/radiusd.conf ืจืง ื”ืžืฉืชืžืฉ ื•ื”ืงื‘ื•ืฆื” ืžื˜ืขืžื ืžืชื—ื™ืœื™ื freeradius, ืžืื– ื”ืฉื™ืจื•ืช ืจื“ื™ื•ืก ืืžื•ืจ ืœื”ื™ื•ืช ืžืกื•ื’ืœ ืœืงืจื•ื ืงื‘ืฆื™ื ื‘ื›ืœ ืกืคืจื™ื•ืช ื”ืžืฉื ื” /ื‘ื™ืช/.

user = root
group = root

ื›ื“ื™ ืœื”ื™ื•ืช ืžืกื•ื’ืœ ืœื”ืฉืชืžืฉ ื‘ืงื‘ื•ืฆื•ืช ื‘ื”ื’ื“ืจื•ืช ืžื–ืœื–ืœ, ื™ืฉ ืœื”ืขื‘ื™ืจ ืชื›ื•ื ื” ืกืคืฆื™ืคื™ืช ืœืกืคืง. ื›ื“ื™ ืœืขืฉื•ืช ื–ืืช, ื‘ืžื“ืจื™ืš raddb/policy.d ืื ื™ ื™ื•ืฆืจ ืงื•ื‘ืฅ ืขื ื”ืชื•ื›ืŸ ื”ื‘ื:

group_authorization {
    if (&LDAP-Group[*] == "CN=vpn_admins,OU=vpn-groups,DC=domain,DC=local") {
            update reply {
                &Fortinet-Group-Name = "vpn_admins" }
            update control {
                &Auth-Type := PAM
                &Reply-Message := "Welcome Admin"
                }
        }
    else {
        update reply {
        &Reply-Message := "Not authorized for vpn"
            }
        reject
        }
}

ืœืื—ืจ ื”ื”ืชืงื ื” freeradius-ldap ื‘ืกืคืจื™ื™ื” raddb/mods-available ื”ืงื•ื‘ืฅ ื ื•ืฆืจ ldap.

ืขืœื™ืš ืœื™ืฆื•ืจ ืงื™ืฉื•ืจ ืกืžืœื™ ืœืกืคืจื™ื™ื” ืžื•ืคืขืœืช raddb/mods.

ln -s /etc/raddb/mods-available/ldap /etc/raddb/mods-enabled/ldap

ืื ื™ ืžืฆื™ื’ ืืช ืชื•ื›ื ื• ื›ื“ืœืงืžืŸ:

ldap {
        server = 'domain.local'
        identity = 'CN=freerad_user,OU=users,DC=domain,DC=local'
        password = "SupeSecretP@ssword"
        base_dn = 'dc=domain,dc=local'
        sasl {
        }
        user {
                base_dn = "${..base_dn}"
                filter = "(sAMAccountname=%{%{Stripped-User-Name}:-%{User-Name}})"
                sasl {
                }
                scope = 'sub'
        }
        group {
                base_dn = "${..base_dn}"
                filter = '(objectClass=Group)'
                scope = 'sub'
                name_attribute = cn
                membership_filter = "(|(member=%{control:Ldap-UserDn})(memberUid=%{%{Stripped-User-Name}:-%{User-Name}}))"
                membership_attribute = 'memberOf'
        }
}

ื‘ืงื‘ืฆื™ื raddb/sites-enabled/default ะธ raddb/sites-enabled/inner-tunnel ื‘ืกืขื™ืฃ ืœืืฉืจ ืื ื™ ืžื•ืกื™ืฃ ืืช ืฉื ื”ืžื“ื™ื ื™ื•ืช ืฉื‘ื” ื™ืฉ ืœื”ืฉืชืžืฉ - group_authorization. ื ืงื•ื“ื” ื—ืฉื•ื‘ื” - ืฉื ื”ืคื•ืœื™ืกื” ืœื ื ืงื‘ืข ืœืคื™ ืฉื ื”ืงื•ื‘ืฅ ื‘ืกืคืจื™ื” ืžื“ื™ื ื™ื•ืช.ื“, ืื‘ืœ ืœืคื™ ื”ื ื—ื™ื” ื‘ืชื•ืš ื”ืงื•ื‘ืฅ ืœืคื ื™ ื”ืคืœื˜ื” ื”ืžืชื•ืœืชืœืช.
ื‘ืงื˜ืข ืœืืžืช ื‘ืื•ืชื ืงื‘ืฆื™ื ืืชื” ืฆืจื™ืš ืœื‘ื˜ืœ ืืช ื”ื”ืขืจื” ืœืฉื•ืจื” ืคืื.

ื‘ืงื•ื‘ืฅ clients.conf ืœืจืฉื•ื ืืช ื”ืคืจืžื˜ืจื™ื ืฉืื™ืชื ื”ื•ื ื™ืชื—ื‘ืจ ืžื–ืœื–ืœ:

client fortigate {
    ipaddr = 192.168.1.200
    secret = testing123
    require_message_authenticator = no
    nas_type = other
}

ืชืฆื•ืจืช ืžื•ื“ื•ืœ pam.d/radiusd:

#%PAM-1.0
auth       sufficient   pam_google_authenticator.so
auth       include      password-auth
account    required     pam_nologin.so
account    include      password-auth
password   include      password-auth
session    include      password-auth

ืืคืฉืจื•ื™ื•ืช ื™ื™ืฉื•ื ื—ื‘ื™ืœื•ืช ื‘ืจื™ืจืช ืžื—ื“ืœ freeradius ั ืžืืžืช ืฉืœ ืœื“ืจื•ืฉ ืžื”ืžืฉืชืžืฉ ืœื”ื–ื™ืŸ ืื™ืฉื•ืจื™ื ื‘ืคื•ืจืžื˜: ืฉื ืžืฉืชืžืฉ ืกื™ืกืžื+OTP.

ืขืœ ื™ื“ื™ ื“ื™ืžื™ื•ืŸ ืžืกืคืจ ื”ืงืœืœื•ืช ืฉื™ื™ืคืœื• ืขืœ ื”ืจืืฉ, ื‘ืžืงืจื” ืฉืœ ืฉื™ืžื•ืฉ ื‘ื—ื‘ื™ืœืช ื‘ืจื™ืจืช ื”ืžื—ื“ืœ freeradius ั ืžืืžืช Google, ื”ื•ื—ืœื˜ ืœื”ืฉืชืžืฉ ื‘ืชืฆื•ืจืช ื”ืžื•ื“ื•ืœ ืคืื ื›ืš ืฉืจืง ื”ืืกื™ืžื•ืŸ ืžืกื•ืžืŸ ืžืืžืช Google.

ื›ืืฉืจ ืžืฉืชืžืฉ ืžืชื—ื‘ืจ, ืžืชืจื—ืฉื™ื ื”ื“ื‘ืจื™ื ื”ื‘ืื™ื:

  • Freeradius ื‘ื•ื“ืง ืื ื”ืžืฉืชืžืฉ ื ืžืฆื ื‘ื“ื•ืžื™ื™ืŸ ื•ื‘ืงื‘ื•ืฆื” ืžืกื•ื™ืžืช, ื•ืื ืžืฆืœื™ื—, ื‘ื•ื“ืง ืืช ืืกื™ืžื•ืŸ ื”-OTP.

ื”ื›ืœ ื ืจืื” ื˜ื•ื‘ ืžืกืคื™ืง ืขื“ ืœืจื’ืข ืฉื‘ื• ื—ืฉื‘ืชื™ "ืื™ืš ืื ื™ ื™ื›ื•ืœ ืœืจืฉื•ื OTP ืขื‘ื•ืจ 300+ ืžืฉืชืžืฉื™ื?"

ื”ืžืฉืชืžืฉ ื—ื™ื™ื‘ ืœื”ืชื—ื‘ืจ ืœืฉืจืช ืขื freeradius ื•ืžื”ื—ืฉื‘ื•ืŸ ืฉืœืš ื•ื”ืคืขืœ ืืช ื”ืืคืœื™ืงืฆื™ื” ืžืืžืช ื’ื•ื’ืœ, ืืฉืจ ื™ืคื™ืง ืงื•ื“ QR ืขื‘ื•ืจ ื”ืืคืœื™ืงืฆื™ื” ืขื‘ื•ืจ ื”ืžืฉืชืžืฉ. ื›ืืŸ ื ื›ื ืกืช ืขื–ืจื”. ืชื™ื‘ืช ืฉืœื™ื ื” ื‘ืงื•ืžื‘ื™ื ืฆื™ื” ืขื .bash_profile.

[root@freeradius ~]# yum install -y shellinabox

ืงื•ื‘ืฅ ื”ืชืฆื•ืจื” ืฉืœ ื”ื“ืžื•ืŸ ื ืžืฆื ื‘ /etc/sysconfig/shellinabox.
ืื ื™ ืžืฆื™ื™ืŸ ืฉื ื™ืฆื™ืื” 443 ื•ืืชื” ื™ื›ื•ืœ ืœืฆื™ื™ืŸ ืืช ื”ืื™ืฉื•ืจ ืฉืœืš.

[root@freeradius ~]#systemctl enable --now shellinaboxd

ื”ืžืฉืชืžืฉ ืฆืจื™ืš ืจืง ืœืขืงื•ื‘ ืื—ืจ ื”ืงื™ืฉื•ืจ, ืœื”ื–ื™ืŸ ืงืจื“ื™ื˜ื™ื ืœื“ื•ืžื™ื™ืŸ ื•ืœืงื‘ืœ ืงื•ื“ QR ืขื‘ื•ืจ ื”ืืคืœื™ืงืฆื™ื”.

ื”ืืœื’ื•ืจื™ืชื ื”ื•ื ื›ื“ืœืงืžืŸ:

  • ื”ืžืฉืชืžืฉ ืžืชื—ื‘ืจ ืœืžื›ื•ื ื” ื‘ืืžืฆืขื•ืช ื“ืคื“ืคืŸ.
  • ืžืกื•ืžืŸ ืื ื”ืžืฉืชืžืฉ ื”ื•ื ืžืฉืชืžืฉ ื“ื•ืžื™ื™ืŸ. ืื ืœื, ืื– ืœื ื ืขืฉื” ื›ืœ ืคืขื•ืœื”.
  • ืื ื”ืžืฉืชืžืฉ ื”ื•ื ืžืฉืชืžืฉ ื‘ื“ื•ืžื™ื™ืŸ, ื”ื—ื‘ืจื•ืช ื‘ืงื‘ื•ืฆืช Administrators ืžืกื•ืžื ืช.
  • ืื ืื™ื ื• ืžื ื”ืœ, ื”ื•ื ื‘ื•ื“ืง ืื Google Authenticator ืžื•ื’ื“ืจ. ืื ืœื, ืื– ื ื•ืฆืจ ืงื•ื“ QR ื•ื”ืชื ืชืง ืžืฉืชืžืฉ.
  • ืื ืœื ืžื•ื’ื“ืจ ืžื ื”ืœ ืžืขืจื›ืช ื•-Google Authenticator, ืคืฉื•ื˜ ื”ืชื ืชืง.
  • ืื ืžื ื”ืœ ืžืขืจื›ืช, ื‘ื“ื•ืง ืฉื•ื‘ ืืช Google Authenticator. ืื ืœื ืžื•ื’ื“ืจ, ื ื•ืฆืจ ืงื•ื“ QR.

ื›ืœ ื”ื”ื™ื’ื™ื•ืŸ ื ืขืฉื” ื‘ืืžืฆืขื•ืช /etc/skel/.bash_profile.

cat /etc/skel/.bash_profile

# .bash_profile

# Get the aliases and functions
if [ -f ~/.bashrc ]; then
        . ~/.bashrc
fi

# User specific environment and startup programs
# Make several commands available from user shell

if [[ -z $(id $USER | grep "admins") || -z $(cat /etc/passwd | grep $USER) ]]
  then
    [[ ! -d $HOME/bin ]] && mkdir $HOME/bin
    [[ ! -f $HOME/bin/id ]] && ln -s /usr/bin/id $HOME/bin/id
    [[ ! -f $HOME/bin/google-auth ]] && ln -s /usr/bin/google-authenticator $HOME/bin/google-auth
    [[ ! -f $HOME/bin/grep ]] && ln -s /usr/bin/grep $HOME/bin/grep
    [[ ! -f $HOME/bin/figlet ]] && ln -s /usr/bin/figlet $HOME/bin/figlet
    [[ ! -f $HOME/bin/rebel.tlf ]] && ln -s /usr/share/figlet/rebel.tlf $HOME/bin/rebel.tlf
    [[ ! -f $HOME/bin/sleep ]] && ln -s /usr/bin/sleep $HOME/bin/sleep
  # Set PATH env to <home user directory>/bin
    PATH=$HOME/bin
    export PATH
  else
    PATH=PATH=$PATH:$HOME/.local/bin:$HOME/bin
    export PATH
fi


if [[ -n $(id $USER | grep "domain users") ]]
  then
    if [[ ! -e $HOME/.google_authenticator ]]
      then
        if [[ -n $(id $USER | grep "admins") ]]
          then
            figlet -t -f $HOME/bin/rebel.tlf "Welcome to Company GAuth setup portal"
            sleep 1.5
            echo "Please, run any of these software on your device, where you would like to setup OTP:
Google Autheticator:
AppStore - https://apps.apple.com/us/app/google-authenticator/id388497605
Play Market - https://play.google.com/stor/apps/details?id=com.google.android.apps.authenticator2&hl=en
FreeOTP:
AppStore - https://apps.apple.com/us/app/freeotp-authenticator/id872559395
Play Market - https://play.google.com/store/apps/details?id=org.fedorahosted.freeotp&hl=en

And prepare to scan QR code.

"
            sleep 5
            google-auth -f -t -w 3 -r 3 -R 30 -d -e 1
            echo "Congratulations, now you can use an OTP token from application as a password connecting to VPN."
          else
            figlet -t -f $HOME/bin/rebel.tlf "Welcome to Company GAuth setup portal"
            sleep 1.5
            echo "Please, run any of these software on your device, where you would like to setup OTP:
Google Autheticator:
AppStore - https://apps.apple.com/us/app/google-authenticator/id388497605
Play Market - https://play.google.com/store/apps/details?id=com.google.android.apps.authenticator2&hl=en
FreeOTP:
AppStore - https://apps.apple.com/us/app/freeotp-authenticator/id872559395
Play Market - https://play.google.com/store/apps/details?id=org.fedorahosted.freeotp&hl=en

And prepare to scan QR code.

"
            sleep 5
            google-auth -f -t -w 3 -r 3 -R 30 -d -e 1
            echo "Congratulations, now you can use an OTP token from application as a password to VPN."
            logout
        fi
      else
        echo "You have already setup a Google Authenticator"
        if [[ -z $(id $USER | grep "admins") ]]
          then
          logout
        fi
    fi
  else
    echo "You don't need to set up a Google Authenticator"
fi

ื”ืชืงื ื” ื—ื–ืงื”:

  • ืื ื—ื ื• ื™ื•ืฆืจื™ื ืจึทื“ึดื™ื•ึผืก-ืฉืจืช

    Freeradius + Google Authenticator + LDAP + Fortigate

  • ืื ื• ื™ื•ืฆืจื™ื ืืช ื”ืงื‘ื•ืฆื•ืช ื”ื“ืจื•ืฉื•ืช, ื‘ืžื™ื“ืช ื”ืฆื•ืจืš, ื‘ืงืจืช ื’ื™ืฉื” ืœืคื™ ืงื‘ื•ืฆื•ืช. ืฉื ื”ืงื‘ื•ืฆื” ืžื•ืคื™ืข ืžื–ืœื–ืœ ื—ื™ื™ื‘ ืœื”ืชืื™ื ืœืงื‘ื•ืฆื” ื”ืžื•ืขื‘ืจืช ืชื›ื•ื ื” ืกืคืฆื™ืคื™ืช ืœืกืคืง Fortinet-Group-Name.

    Freeradius + Google Authenticator + LDAP + Fortigate

  • ืขืจื™ื›ืช ื”ื“ืจื•ืฉ SSL-ืคื•ืจื˜ืœื™ื.

    Freeradius + Google Authenticator + LDAP + Fortigate

  • ื”ื•ืกืคืช ืงื‘ื•ืฆื•ืช ืœืžื“ื™ื ื™ื•ืช.

    Freeradius + Google Authenticator + LDAP + Fortigate

ื”ื™ืชืจื•ื ื•ืช ืฉืœ ืคืชืจื•ืŸ ื–ื”:

  • ื ื™ืชืŸ ืœื‘ืฆืข ืื™ืžื•ืช ื‘ืืžืฆืขื•ืช OTP ืžื•ืคืขืœ ืžื–ืœื–ืœ ืคืชืจื•ืŸ ืงื•ื“ ืคืชื•ื—.
  • ื”ืžืฉืชืžืฉ ืื™ื ื• ืžื–ื™ืŸ ืกื™ืกืžืช ื“ื•ืžื™ื™ืŸ ื‘ืขืช โ€‹โ€‹ื—ื™ื‘ื•ืจ ื‘ืืžืฆืขื•ืช VPN, ืžื” ืฉืžืคืฉื˜ ืžืขื˜ ืืช ืชื”ืœื™ืš ื”ื—ื™ื‘ื•ืจ. ืงืœ ื™ื•ืชืจ ืœื”ื–ื™ืŸ ืืช ื”ืกื™ืกืžื” ื‘ืช 6 ื”ืกืคืจื•ืช ืžื–ื• ืฉืžืกืคืงืช ืžื“ื™ื ื™ื•ืช ื”ืื‘ื˜ื—ื”. ื›ืชื•ืฆืื” ืžื›ืš, ืžืกืคืจ ื”ื›ืจื˜ื™ืกื™ื ืขื ื”ื ื•ืฉื: "ืื ื™ ืœื ื™ื›ื•ืœ ืœื”ืชื—ื‘ืจ ืœ-VPN" ื™ื•ืจื“.

ื .ื‘. ืื ื• ืžืชื›ื ื ื™ื ืœืฉื“ืจื’ ืืช ื”ืคืชืจื•ืŸ ื”ื–ื” ืœืื™ืžื•ืช ื“ื•-ื’ื•ืจืžื™ ืžืœื ืขื ืืชื’ืจ-ืชื’ื•ื‘ื”.

ืขื“ื›ื•ืŸ:

ื›ืคื™ ืฉื”ื•ื‘ื˜ื—, ืฉื™ื ื™ืชื™ ืื•ืชื• ืœืืคืฉืจื•ืช ื”ืืชื’ืจ-ืชื’ื•ื‘ื”.
ืื–:
ื‘ืงื•ื‘ืฅ /etc/raddb/sites-enabled/default ืกึธืขึดื™ืฃ ืœืืฉืจ ื”ื•ื ื›ื“ืœืงืžืŸ:

authorize {
    filter_username
    preprocess
    auth_log
    chap
    mschap
    suffix
    eap {
        ok = return
    }
    files
    -sql
    #-ldap
    expiration
    logintime
    if (!State) {
        if (&User-Password) {
            # If !State and User-Password (PAP), then force LDAP:
            update control {
                Ldap-UserDN := "%{User-Name}"
                Auth-Type := LDAP
            }
        }
        else {
            reject
        }
    }
    else {
        # If State, then proxy request:
        group_authorization
    }
pap
}

ืžื“ื•ืจ ืœืืžืช ืขื›ืฉื™ื• ื ืจืื” ื›ืš:

authenticate {
        Auth-Type PAP {
                pap
        }
        Auth-Type CHAP {
                chap
        }
        Auth-Type MS-CHAP {
                mschap
        }
        mschap
        digest
        # Attempt authentication with a direct LDAP bind:
        Auth-Type LDAP {
        ldap
        if (ok) {
            update reply {
                # Create a random State attribute:
                State := "%{randstr:aaaaaaaaaaaaaaaa}"
                Reply-Message := "Please enter OTP"
                }
            # Return Access-Challenge:
            challenge
            }
        }
        pam
        eap
}

ื›ืขืช ื”ืžืฉืชืžืฉ ืžืื•ืžืช ื‘ืืžืฆืขื•ืช ื”ืืœื’ื•ืจื™ืชื ื”ื‘ื:

  • ื”ืžืฉืชืžืฉ ืžื–ื™ืŸ ื–ื™ื›ื•ื™ ื“ื•ืžื™ื™ืŸ ื‘ืœืงื•ื— ื”-VPN.
  • Freeradius ื‘ื•ื“ืง ืืช ืชืงืคื•ืช ื”ื—ืฉื‘ื•ืŸ ื•ื”ืกื™ืกืžื”
  • ืื ื”ืกื™ืกืžื” ื ื›ื•ื ื”, ื ืฉืœื—ืช ื‘ืงืฉื” ืœืงื‘ืœืช ืืกื™ืžื•ืŸ.
  • ื”ืืกื™ืžื•ืŸ ืžืื•ืžืช.
  • ืจื•ื•ื—).

ืžืงื•ืจ: www.habr.com

ื”ื•ืกืคืช ืชื’ื•ื‘ื”