/rootДзен'nix→ Скрипт запуска vpn для mac os и не только

, , , ,

10 июня, 2025

Скрипт запуска vpn для mac os и не только

Привет! В Mac OS чаще всего используют программу Tunnelblick для подключения по vpn. Я тоже ей пользовался и это было отвратительно, может у меня кривые руки и я не сумел ее грамотно настроить ( ну те проставить правильно галочки в gui интерефейсе ), но в целом получить от нее стабильности и простоты у меня так и не получилось.

Возможно эта программное обеспечение для подключения к vpn удобно когда тебе нужно подключать по 1 vpn для просмотра ютубчика на обеде ( хотя vpn для этого не нужен, достаточно настроить анонимный proxy ) но когда требуется что-то более сложное Tunnelblick начинает хромать.

Стандартная ситуация, вы работаете в крупной компании и вам требуется использовать корпоративные сервисы, которые развернуты внутри сети. Для этой цели вам выдали vpn.

Так же внутри компании есть разные отделы, например завязанные на какие-то конкретные проекты и внутри корпоративной сети у вас должен так же быть отдельный доступ к внутренним сервисам вашего отдела проекта в котором вы работаете, таким образом вам уже требуется подключаться к 2 vpn сетям одновременно приплюсуем к этому что вы живете в стране в которой часть ресурсов заблокирована из-за непростой геополитической ситуации и вам уже потребуется 3 vpn которые должны работать и не конфликтовать друг с другом.

Tunnelblick с 3 VPN уже работает плохо, давайте еще добавим, что VPN тоже не работает стабильно так как время от времени провайдер тестирует оборудование фильтрации VPN и связь пропадает. Это катастрофа, если вам требуется VPN для работы.

Допустим, еще в вашей компании время от времени обновляются ovpn и его нужно скачивать по специальной ссылке и таким образом, используя гуи приложения для vpn вам придется время от времени заниматься удалением, добавлением и если вы используете еще sock5 для своего подключения — правками ovpn файлов.

Каждый раз после того как Tunnelblick зависал, я грустил и вспоминал времена когда у меня был arch linux и для подключения к vpn я использовал простой скрипт на bash.

Последней каплей стала ситуация когда после обновления ОС, вместе с зависанием Tunnelblick стала пропадать сеть так что требовалась перезагрузка макбука для того чтобы сеть снова появилась ( позже я разобрался с этим и это оказалось, что это из-за того что Tunnelblick не мог правильно возвращал DNS сервера на место ). Это стало последней каплей терпения и я решил окончательно решить этот вопрос и удалив с радостью Tunnelblick написал свой скрипт для подключения к VPN, чем хочу поделится тут.

#!/bin/bash

VPN_PID="$HOME/.vpn/vpn.pid"
LOG_FILE="$HOME/.vpn/vpn_download.log"

# в этом файле нужно прописать ссылки ( каждая на своей строке по которым скрипт будет забирать обновленные ovpn файлы
# содержимое файла ~/.vpn/update.txt должно иметь вот такой вид: 
# https://corporate.site/share/project.ovpn
# https://corporate.size/share/corporate.ovpn
URL_LIST_FILE="$HOME/.vpn/update.txt"

# пути к .ovpn файлам 
TARGET_DIR="$HOME/.vpn/configs"
VPN_CONFIG_1="$TARGET_DIR/corporate.ovpn"
VPN_CONFIG_2="$TARGET_DIR/project.ovpn"
VPN_CONFIG_3="$TARGET_DIR/private.ovpn"

# логин и пароль от корпоративного VPN
# содержимое файла ~/.vpn/password.auth
# <login_vpn>
# <password_vpn> 
VPN_PASSWORD="$HOME/.vpn/password.auth"

# пусть к команде openvpn
# в моем случае я ставил openvpn для mac os через homebrew
VPN_COMMAND="/opt/homebrew/sbin/openvpn"

# замените на ваш DNS
DNS_SERVER="8.8.8.8"

# замените на вашу сеть
NETWORK_SERVICE="Wi-Fi"

# таймауты и настройки проверки подключений к vpn
CHECK_TIMEOUT=10
MAX_ATTEMPTS=3
CONNECT_TIMEOUT=30

# Shadowsocks параметры, как настраивать я писал тут: 
# Как настроить Shadowsocks - https://killercoder.ru/maskirovka-trafika-cherez-shadowsocks
# Как установить Shadowsocks - https://killercoder.ru/ustanavlivaem-shadowsocks-na-svoj-server
SHADOWSOCKS=false
SHADOWSOCKS_PROXY="socks5://127.0.0.1:1080"

# Парсинг аргументов
# Поддерживает 1 аргумент --sock ( подключение через sock proxy )
while [[ $# -gt 0 ]]; do
    case "$1" in
        --sock)
            SHADOWSOCKS=true
            shift
            ;;
        *)
            break
            ;;
    esac
done

# загружает обновленные файлы ovpn с поддержкой загрузки через sock5 ( если дела совсем плохи.. )
download_file() {
    local url=$1
    local filename=$(basename "$url")
    local target_path="$TARGET_DIR/$filename"
    local retry=0
    
    echo "$(date '+%Y-%m-%d %H:%M:%S') - Начало загрузки $filename" >> "$LOG_FILE"
    
    while [ $retry -lt $MAX_ATTEMPTS ]; do
        if [ "$SHADOWSOCKS" = true ]; then
            if curl -sSL --fail --connect-timeout $CONNECT_TIMEOUT --proxy "$SHADOWSOCKS_PROXY" "$url" -o "$target_path"; then
                echo "$(date '+%Y-%m-%d %H:%M:%S') - Успешно (через Shadowsocks): $filename сохранен в $target_path" >> "$LOG_FILE"
                echo "✅ $filename успешно загружен через Shadowsocks"
                return 0
            fi
        else
            if curl -sSL --fail --connect-timeout $CONNECT_TIMEOUT "$url" -o "$target_path"; then
                echo "$(date '+%Y-%m-%d %H:%M:%S') - Успешно: $filename сохранен в $target_path" >> "$LOG_FILE"
                echo "✅ $filename успешно загружен"
                return 0
            fi
        fi
        
        ((retry++))
        echo "$(date '+%Y-%m-%d %H:%M:%S') - Попытка $retry/$MAX_ATTEMPTS: Ошибка загрузки $filename" >> "$LOG_FILE"
        sleep 2
    done
    
    echo "$(date '+%Y-%m-%d %H:%M:%S') - Ошибка: не удалось загрузить $filename после $MAX_ATTEMPTS попыток" >> "$LOG_FILE"
    echo "❌ Не удалось загрузить $filename после $MAX_ATTEMPTS попыток"
    return 1
}

update() {

    if [ ! -f "$URL_LIST_FILE" ]; then
        echo "❌ Файл $URL_LIST_FILE не найден!" | tee -a "$LOG_FILE"
        exit 1
    fi

    stop_vpn
    sleep 2
    echo "=== Начало загрузки VPN-конфигов ===" | tee -a "$LOG_FILE"
    echo "Чтение URL из файла: $URL_LIST_FILE" | tee -a "$LOG_FILE"

    success_count=0
    fail_count=0

    while IFS= read -r url || [ -n "$url" ]; do
        if [[ -z "$url" || "$url" == \#* ]]; then
            continue
        fi
    
        download_file "$url"
        if [ $? -eq 0 ]; then
            ((success_count++))
        else
            ((fail_count++))
        fi
    done < "$URL_LIST_FILE"

    echo "=== Загрузка завершена ===" | tee -a "$LOG_FILE"
    echo "Успешно: $success_count файлов" | tee -a "$LOG_FILE"
    echo "Не удалось: $fail_count файлов" | tee -a "$LOG_FILE"

    if [ $fail_count -gt 0 ]; then
        exit 1
    else
        start_vpn       
        exit 0
    fi
}

# команда запуска личного VPN 
start_private() {
    stop_vpn
    sleep 2
    echo "🔒 Запускаю VPN private..."
    
    if [ "$SHADOWSOCKS" = true ]; then
        echo "Использую Shadowsocks proxy для подключения"
        sudo $VPN_COMMAND \
            --config "$VPN_CONFIG_3" \
            --socks-proxy 127.0.0.1 1080 \
            --log-append /var/log/openvpn.log &
	ALL_PROXY=$SHADOWSOCKS_PROXY
    else
        sudo $VPN_COMMAND \
            --config "$VPN_CONFIG_3" \
            --log-append /var/log/openvpn.log &
    fi
    
    echo "VPN(private)" > $VPN_PID
}

# команда запуска корпоративного VPN
start_vpn() {
    stop_vpn  
    echo "🔒 Запускаю VPN corporate..."

    # Проверка наличия нужных файлов
    if [[ ! -f "$VPN_CONFIG_1" ]]; then
        echo "❌ Ошибка: файл конфигурации $VPN_CONFIG_1 не найден!"
        exit 1
    fi

    if [[ ! -f "$VPN_CONFIG_2" ]]; then
        echo "❌ Ошибка: файл конфигурации $VPN_CONFIG_2 не найден!"
        exit 1
    fi

    if [[ ! -f "$VPN_PASSWORD" ]]; then
        echo "❌ Ошибка: файл аутентификации $VPN_PASSWORD не найден!"
        exit 1
    fi

    if [ "$SHADOWSOCKS" = true ]; then
        echo "Использую Shadowsocks proxy для подключения"
        sudo $VPN_COMMAND \
            --config "$VPN_CONFIG_1" \
            --auth-user-pass "$VPN_PASSWORD" \
            --socks-proxy 127.0.0.1 1080 \
            --log-append /var/log/openvpn.log &

        sudo $VPN_COMMAND \
            --config "$VPN_CONFIG_2" \
            --auth-user-pass "$VPN_PASSWORD" \
            --socks-proxy 127.0.0.1 1080 \
            --log-append /var/log/openvpn.log &
    else
        sudo $VPN_COMMAND \
            --config "$VPN_CONFIG_1" \
            --auth-user-pass "$VPN_PASSWORD" \
            --log-append /var/log/openvpn.log &

        sudo $VPN_COMMAND \
            --config "$VPN_CONFIG_2" \
            --auth-user-pass "$VPN_PASSWORD" \
            --log-append /var/log/openvpn.log &
    fi

    # тут же проверяем подключение
    attempt=1
    while [ $attempt -le $MAX_ATTEMPTS ]; do
        echo "Проверка подключения (попытка $attempt/$MAX_ATTEMPTS)..."
    
        if check_vpn_connection; then
            echo "VPN успешно подключен!"
        
            if set_dns; then
                export CURRENT_VPN="work"
                echo "VPN(corporate)" > $VPN_PID
                exit 0
            else
                exit 1
            fi
        fi
    
        sleep $CHECK_TIMEOUT
        ((attempt++))
    done
}

stop_vpn() {
    echo "🛑 Останавливаю VPN..."
    sudo networksetup -setdnsservers $NETWORK_SERVICE empty
    sudo pkill -SIGINT openvpn
    rm -f $VPN_PID
}

log() {
    sudo tail -f /var/log/openvpn.log
}

empty_dns() {
    sudo networksetup -setdnsservers $NETWORK_SERVICE empty
}

status_vpn() {
    if pgrep -x "openvpn" > /dev/null; then
        echo "✅ VPN работает (PID: $(pgrep -x "openvpn"))."
        if [ -f "$VPN_PID" ]; then
            echo "Текущий профиль: $(cat "$VPN_PID")"
        fi
        if [ "$SHADOWSOCKS" = true ]; then
            echo "Используется Shadowsocks proxy"
        fi
    else
        echo "❌ VPN не запущен."
    fi
}

clear_utun() {
    for i in $(ifconfig | grep '^utun' | cut -d: -f1); do
        if ! lsof -i | grep -q "$i"; then
            echo "Удаляю $i..."
            sudo ifconfig "$i" down
            sudo ifconfig "$i" destroy
        fi
    done
}

# функция остановки DNS
set_dns() {
    echo "Устанавливаю DNS $DNS_SERVER для $NETWORK_SERVICE..."
    sudo networksetup -setdnsservers "$NETWORK_SERVICE" "$DNS_SERVER"
    
    if networksetup -getdnsservers "$NETWORK_SERVICE" | grep -q "$DNS_SERVER"; then
        echo "DNS успешно изменён"
        return 0
    else
        echo "Ошибка: не удалось изменить DNS"
        return 1
    fi
}

# функция проверки соединения
check_vpn_connection() {
    if ifconfig | grep -q '^utun\|^tun'; then
        if ping -c 1 -t 2 10.34.96.1 >/dev/null 2>&1; then
            return 0
        fi
    fi
    return 1
}

case "$1" in
    corp)
        start_vpn
        ;;
    stop)
        stop_vpn
        ;;
    restart)
        stop_vpn
        sleep 2
        start_vpn
        ;;
    status)
        status_vpn
        ;;
    private)
        start_private
        ;;
    empty_dns)
        empty_dns
        ;;
    log)
        log
        ;;
    clear_utun)
        clear_utun
        ;;
    update)
        update
        ;;
    *)
        echo "Использование: $0 {start|stop|restart|status|update|clear_utun|empty_dns|log} [--sock]"
        echo ""
        echo "Опции:"
        echo "  --sock    Использовать Shadowsocks proxy для подключения"
        exit 1
esac

Все подробности как что работает есть в комментариях в коде

Есть еще несколько вещей, которые можно сделать для удобства. Например, чтобы каждый раз не вводить пароль при запросе sudo через visudo добавляете

user ALL=(ALL) NOPASSWD: /opt/homebrew/sbin/openvpn
user ALL=(ALL) NOPASSWD: /usr/sbin/networksetup
user ALL=(ALL) NOPASSWD: /opt/homebrew/sbin/openvpn
user ALL=(ALL) NOPASSWD: /opt/homebrew/sbin/openvpn
user ALL=(ALL) NOPASSWD: /usr/sbin/networksetup
user ALL=(ALL) NOPASSWD: /usr/bin/pkill -SIGINT openvpn

Вместо user указываете своего пользователя, вместо команд правильные пути в вашей системе. Правильный путь можно узнать командой:

$ which openvpn

И так же по-аналогии с sock5 можно сделать вывод подключения в командной строке, если пользуетесь оболочкой zsh

vpn_status() {
  if [[ -f ~/.vpn/vpn.pid ]]; then
    local vpn_name=$(cat ~/.vpn/vpn.pid)
    echo "🟢 VPN: %F{green}$vpn_name%f"  # Иконка + название VPN
  else
    echo "🔴 VPN: %F{red}DISCONECT%f"
  fi
}

PROMPT='
... %F{yellow}%f $(vpn_status) ...

На этом все.. Если есть вопросы, пишите в телегу а так же подписывайтесь на обновление сайта в группу телеграм

Так же может быть интересно:

Устанавливаем Shadowsocks на свой сервер
Поиск файла по содержимому этого файла
Групповое переименование файлов по маске
Блокировка Газпромбанка, но выход есть.