この記事は 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-Password
と CHAP-Response
が入っていることがわかります。
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 として公開されているプロトコル実装を使っていることもありますが、社内で独自に実装しているものもあり、プロトコル実装に興味がある方も募集しております。