OpenVPN 서버: 스크립트를 통해 정적 IP 주소 할당

18045 단어 openvpn
저는 최근에 각 클라이언트에 고정 IP 주소를 할당해야 하는 OpenVPN 서버를 설정해야 했습니다. IP 주소는 클라이언트 이름 및 기타 데이터와 함께 데이터베이스 테이블에 저장되었습니다. client-config-dir 지시문을 사용하여 클라이언트별 구성 파일을 통해 정적 구성을 사용하는 대신 동적으로 수행할 수 있는 방법을 찾았습니다. 나는 결국 client-connect 스크립트를 사용하여 데이터베이스에서 직접 IP를 할당하기로 결정했지만 문서가 다소 부족하다는 것을 알았습니다.

몇 가지 다른 작업에 사용되는 clients라는 데이터베이스 테이블이 있으며 기본적으로 다음과 같은 구조를 갖습니다.

CREATE TABLE clients (
  id SERIAL PRIMARY KEY,
  name VARCHAR(255) NOT NULL,
  ip inet NOT NULL,
  CONSTRAINT clients_name_unique UNIQUE (name),
  CONSTRAINT clients_ip_unique UNIQUE (ip)
);


클라이언트가 연결되면 X509 일반 이름을 기반으로 IP 주소를 찾아 클라이언트에 푸시하려고 합니다.
client-config-dir 옵션을 사용하는 경우 클라이언트의 일반 이름과 동일한 이름으로 구성된 디렉토리 내에 파일을 만들고 ifconfig-push IP_ADDRESS SUBNET_MASK 를 사용하여 이 작업을 수행합니다. 이는 주 서버 구성 파일 내의 주석에 언급된 것을 포함하여 매우 잘 문서화된 접근 방식입니다. 문제는 내가 이미 데이터베이스 테이블을 유지 관리할 때 이러한 파일을 유지 관리하는 데 추가 작업이 추가된다는 것입니다.

다른 접근 방식은 OpenVPN 서버가 다양한 이벤트를 처리할 때 실행하도록 구성할 수 있는 가능한 많은 스크립트 중 하나(특히 client-connect 스크립트)를 사용하는 것입니다.

옵션을 탐색하는 동안 StackExchange 네트워크에서 예제 스크립트를 찾는 사람들의 몇 가지 질문을 발견했고 결국 Reference manual for OpenVPN 2.4 및 보다 구체적으로 Scripting and environment variables section 을 찾았습니다. 이렇게 하면 각 유형의 스크립트에 사용할 수 있는 환경 변수를 설명할 수 있지만 연결 구성 자체에 실제로 영향을 미칠 수 있는 방법을 설명하는 데는 별 도움이 되지 않습니다.

OpenVPN 서버는 실제로 클라이언트에 대한 구성을 나타내는 파일의 경로인 구성된 스크립트에 인수를 전달하는 것으로 나타났습니다. 따라서 흐름은 기본적으로 다음과 같이 끝납니다.
  • common_name 환경 변수가 설정된 경우 가져옵니다.
  • common_name 값을 사용하여 클라이언트의 IP 주소를 찾습니다.
  • IP 주소가 발견되면 프로그램의 첫 번째 인수에 지정된 파일을 열고 텍스트ifconfig-push IP_ADDRESS SUBNET_MASK를 파일에 씁니다.

  • 하지만 이 접근 방식에 대한 몇 가지 참고 사항이 있습니다.
  • 스크립트가 0이 아닌 종료 코드와 함께 종료되면 클라이언트의 연결 시도가 실패합니다.
  • running OpenVPN Connect v3 on Windows in service daemon mode Windows의 서비스 복구 옵션이 서비스를 복구하지 않는 경우 장애로 인해 심각한 충돌이 발생합니다. 이로 인해 내 스크립트는 오류가 발생한 경우에도 항상 종료 코드 0을 반환합니다. 이것은 직관적이지 않지만 필요합니다.

  • OpenVPN 서버 구성 파일에 있는 구성 값의 최대 길이는 256자이므로 명령줄에서 데이터 소스 이름을 전달하면 안 됩니다. 이 경우 실제 스크립트를 호출하는 래퍼 스크립트를 작성하는 것이 가장 좋습니다.
  • 스크립트에서 stderr/stdout으로의 모든 출력은 OpenVPN 서버에 의해 바로 작성됩니다. 즉, OpenVPN 서버를 실행하는 systemd가 있는 Linux 서버에 있는 경우 스크립트의 로그를 journald에서 찾을 수 있습니다!

  • 결국 내가 택한 경로는 bash 스크립트를 호출하는 비교적 간단한 Go였습니다. bash 스크립트는 /usr/local/bin/resolve-vpn-ip.sh에 저장되고 실행 가능합니다.

    #!/usr/bin/env bash
    set -Eeuo pipefail
    
    DSN="postgresql://..."
    
    /usr/local/bin/resolve-vpn-ip -db="$DSN" "$@"
    


    실제/usr/local/bin/resolve-vpn-ip 프로그램은 Go 바이너리입니다. 동일한 데이터베이스와 통신하는 다른 시스템의 일부 공통 코드를 재사용하기 때문입니다. 기본적으로 다음과 같이 요약됩니다.

    package main
    
    import (
        "context"
        "flag"
        "fmt"
        "log"
        "net"
        "os"
        "os/signal"
        "syscall"
    
        "github.com/jackc/pgx/v4"
    )
    
    func main() {
        log.SetFlags(log.LstdFlags)
    
        flags := flag.NewFlagSet("resolve-vpn-ip", flag.ExitOnError)
        dsnFlag := flags.String("db", "", "the database connection string to use to connect to the database")
    
        if err := flags.Parse(os.Args[1:]); err != nil {
            flags.PrintDefaults()
    
            os.Exit(0)
        }
    
        remainingArgs := flags.Args()
    
        if len(remainingArgs) < 1 {
            log.Println("no arguments after flags, OpenVPN did not pass in a configuration file")
    
            os.Exit(0)
        }
    
        commonName, found := os.LookupEnv("common_name")
    
        if !found || commonName == "" {
            log.Println("common_name environment variable is not set or is empty")
    
            os.Exit(0)
        }
    
        log.Printf("getting IP address for client: %s\n", commonName)
    
        ctx, _ := signal.NotifyContext(context.Background(), os.Interrupt, syscall.SIGTERM)
    
        ip, err := getIpForClient(ctx, *dsnFlag, commonName)
    
        if err != nil {
            log.Printf("error getting IP address for client: %s; error: %s\n", commonName, err)
    
            os.Exit(0)
        }
    
        f, err := os.OpenFile(remainingArgs[0], os.O_APPEND|os.O_WRONLY|os.O_CREATE, os.ModePerm)
    
        if err != nil {
            log.Printf(
                "failed to open config file for client: %s; file: %s; error: %s\n",
                commonName,
                remainingArgs[0],
                err,
            )
    
            os.Exit(0)
        }
    
        defer func(toClose *os.File) {
            _ = toClose.Close()
        }(f)
    
        if _, err = fmt.Fprintf(f, "ifconfig-push %s 255.255.255.0\n", ip.String()); err != nil {
            log.Printf(
                "failed to write to config file for client: %s; file: %s; error: %s\n",
                commonName,
                remainingArgs[0],
                err,
            )
    
            os.Exit(0)
        }
    }
    
    func getIpForClient(ctx context.Context, dsn, commonName string) (*net.IP, error) {
        conn, err := pgx.Connect(ctx, dsn)
    
        if err != nil {
            return nil, err
        }
    
        defer func(c *pgx.Conn) {
            _ = conn.Close(ctx)
        }(conn)
    
        var ip net.IP
    
        err = conn.QueryRow(ctx, `SELECT ip FROM clients WHERE name = $1;`, commonName).Scan(&ip)
    
        switch err {
        case nil:
            return &ip, nil
        case pgx.ErrNoRows:
            return nil, fmt.Errorf("no rows found for client: %s", commonName)
        default:
            return nil, fmt.Errorf("error getting ip for client: %s; error: %w", commonName, err)
        }
    }
    


    OpenVPN 서버 구성 파일에서 다음과 같은 내용을 추가하여 내 스크립트를 실행하도록 서버를 구성할 수 있습니다.

    client-connect "/usr/local/bin/resolve-vpn-ip.sh"
    


    그리고 journalctl -f /usr/local/bin/resolve-vpn-ip 와 같은 명령을 실행하여 journald를 통해 언제든지 내 로그를 추적할 수 있습니다.

    좋은 웹페이지 즐겨찾기