Go で RADIUS をしゃべる

この記事は BBSakura Networks Advent Calendar 2024 の 12/7(土)の記事です。

こんにちは。BBSakura Networks CTO の日下部(@higebu)です。

RADIUS(Remote Authentication Dial In User Service)は RFC2865 で定義されている AAA(Authentication, Authorization, and Accounting)のためのプロトコルです。 1997 年に RFC となった古いプロトコルですが、モバイルネットワーク等では現在も現役で使われています。

今回は Go で書いているモバイルコアなどで RADIUS をしゃべらないといけなくなった方向けに便利なパッケージの紹介と、FreeRADIUS との通信方法、3GPP 独自のアトリビュートの追加方法を説明します。

layeh.com/radius の簡単な紹介

layeh/radius は RADIUS のクライアント・サーバの両方に対応したパッケージです。

アトリビュートのパーサ、シリアライザのコードは radius-dict-gen というツールで、自動生成されています。このリポジトリにないアトリビュートを追加したいときは、 RADIUS のディクショナリファイルがあれば、簡単に追加することが可能です。

Go で RADIUS クライアントを実装してみる

適当なディレクトリを作って、 go mod init しておきます。

また、 layeh/radius の README に書いてある通り、作ったディレクトリで go get -u layeh.com/radius を実行しておきます。

Access-Request を投げて CHAP 認証する main.go は下記のようになります。

package main

import (
        "context"
        "crypto/md5"
        "crypto/rand"
        "flag"
        "fmt"
        "io"

        "layeh.com/radius"
        "layeh.com/radius/rfc2865"
)

var (
        secret   string
        addr     string
        user     string
        password string
)

func init() {
        flag.StringVar(&secret, "secret", "secret", "RADIUS secret")
        flag.StringVar(&addr, "addr", "localhost:1812", "RADIUS server address")
        flag.StringVar(&user, "user", "user", "RADIUS user")
        flag.StringVar(&password, "password", "password", "RADIUS password")
}

func main() {
        flag.Parse()

        // Calculate the CHAP response from the password and challenge
        challenge := make([]byte, 16)
        rand.Read(challenge)
        h := md5.New()
        h.Write([]byte{0})
        io.WriteString(h, password)
        h.Write(challenge)
        response := h.Sum(nil)

        // Create and send Access-Request packet
        packet := radius.New(radius.CodeAccessRequest, []byte(secret))
        rfc2865.UserName_SetString(packet, user)
        rfc2865.CHAPPassword_Set(packet, append([]byte{0}, response...))
        rfc2865.CHAPChallenge_Set(packet, challenge)
        resp, err := radius.Exchange(context.Background(), packet, addr)
        if err != nil {
                panic(err)
        }
        fmt.Println(resp.Code)
}

README に PAP 認証の場合のサンプルコードがあるので、 CHAP 認証にしてみました。 コマンド実行時に RADIUS サーバのシークレット、認証したいユーザのユーザ名、パスワードを入力したいので、フラグを用意しています。

このコードで実際に認証を行えるかどうかを試すには FreeRADIUS を立てるのが簡単なので、 Docker を使って立てましょう。

今いるディレクトリの下に下記のファイルを用意しておきます。

  • freeradius/raddb/clients.conf
client localhost {
        ipaddr = 127.0.0.1
        secret = secret
}
client dockernet {
        ipaddr = 172.17.0.0/16
        secret = secret
}
  • freeradius/raddb/mods-config/files/authorize
bob        Cleartext-Password := "hello"
           Service-Type = Framed-User

FreeRADIUS を Docker で動かすコマンドは下記の通りです。

docker run --rm -d \
    --name freeradius \
    -v ./freeradius/raddb/clients.conf:/etc/raddb/clients.conf \
    -v ./freeradius/raddb/mods-config/files/authorize:/etc/raddb/mods-config/files/authorize \
    -p 1812-1813:1812-1813/udp \
    freeradius/freeradius-server

コンテナを起動できたら、先ほど作成したコードを実行してみましょう。

$ go run main.go -secret secret -user bob -password hello
Access-Accept

このように、 Access-Accept と表示されれば成功です。

パケットの中身を見てみると、きちんと CHAP-PasswordCHAP-Response が入っていることがわかります。

Access-Request

Go で RADIUS サーバを実装してみる

Disconnect-Request を受信したら Disconnect-ACK を返すだけのコードは下記のようになります。

package main

import (
        "log"

        "layeh.com/radius"
        "layeh.com/radius/rfc2865"
)

func main() {
        handler := func(w radius.ResponseWriter, r *radius.Request) {
                if r.Code == radius.CodeDisconnectRequest {
                        username := rfc2865.UserName_GetString(r.Packet)
                        log.Printf("Disconnect %s", username)
                        code := radius.CodeDisconnectACK
                        log.Printf("Writing %v to %v", code, r.RemoteAddr)
                        w.Write(r.Response(code))
                }
        }

        server := radius.PacketServer{
                Handler:      radius.HandlerFunc(handler),
                SecretSource: radius.StaticSecretSource([]byte(`secret`)),
        }

        log.Printf("Starting server on :1812")
        if err := server.ListenAndServe(); err != nil {
                log.Fatal(err)
        }
}

go build して実行するか、 go run main.go でサーバが起動できます。

上記の通り、ハンドラ内で RADIUS の Code に応じて処理を分岐する必要があります。

ハンドラの登録時に、 RADIUS の Code を指定できるともっと便利になりそうなので、ハンドラパターンでよくある、 ServeMuxこちら で開発中です。

これを使うと下記のようになります。

        handler := func(w radius.ResponseWriter, r *radius.Request) {
                username := rfc2865.UserName_GetString(r.Packet)
                code := radius.CodeDisconnectACK
                log.Printf("Disconnect %s", username)
                log.Printf("Writing %v to %v", code, r.RemoteAddr)
                w.Write(r.Response(code))
        }

        radius.HandleFunc(radius.CodeDisconnectRequest, handler)

        server := radius.PacketServer{
                SecretSource: radius.StaticSecretSource([]byte(`secret`)),
        }

アトリビュートを追加してみる

今回は 3GPP TS 29.061 で定義されているアトリビュートを追加しようと思います。

まず、今回作っているリポジトリのトップに ts29061 という名前のディレクトリを作り、そこにディクショナリファイル dictionary.3gpp を置きます。 今回はわかりやすさのために IMSI のみ追加する内容にしています。

VENDOR          3GPP                            10415
BEGIN-VENDOR    3GPP

ATTRIBUTE       IMSI                                    1       string

END-VENDOR      3GPP

ディクショナリについて、詳しくは FreeRADIUS のサイト に説明があります。

コードを生成するには下記のコマンドを実行します。

go run layeh.com/radius/cmd/radius-dict-gen@master -package ts29061 -output generated.go dictionary.3gpp

成功すると、 generated.go というファイルができているはずです。これを使うと下記のように、 3GPP TS 29.061 に定義されているアトリビュートを使えるようになります。

ts29061.ThreeGPPIMSI_Add(packet, []byte(imsi))

最後に

今回は Go で RADIUS のクライアント・サーバの実装をしたいときに使える、 layeh/radius を紹介しました。

BBSakura Networks では fiorix/go-diameter のように OSS として公開されているプロトコル実装を使っていることもありますが、社内で独自に実装しているものもあり、プロトコル実装に興味がある方も募集しております。