Compare commits
59 Commits
main
...
eap-but-ac
Author | SHA1 | Date | |
---|---|---|---|
06848be14b | |||
4bae3bbe60 | |||
e33f839d7f | |||
f5eb827d14 | |||
9045f5ba73 | |||
7b97e92094 | |||
3027cdcc4b | |||
67f627a925 | |||
f1101e0c01 | |||
fb01a117ad | |||
fad18db70b | |||
e0c837257c | |||
2a567ccc85 | |||
e36373ceab | |||
d8a625be03 | |||
4d944f7444 | |||
c49274042b | |||
10fc15ffe0 | |||
7c996d9d9d | |||
5d25f68b71 | |||
8da54d5811 | |||
4571f5e644 | |||
ee234ea3aa | |||
82c177b7eb | |||
1155ccb3e8 | |||
1575b96262 | |||
19bb77638a | |||
d6cf129eaa | |||
b6686cff14 | |||
8cf8f1e199 | |||
50c50c4109 | |||
51f4a8d83d | |||
3ada3a7e0e | |||
fa06c9fe4e | |||
2a024238fe | |||
91c87b7c3c | |||
318443f270 | |||
ac88784089 | |||
855afa7b9f | |||
240abfef41 | |||
03075f1890 | |||
5bc0ed6e11 | |||
8f4cfc28c7 | |||
6d77eaaab7 | |||
9cee59537c | |||
fc5c0e2789 | |||
573446689f | |||
fd4bfe604d | |||
06e76a5b37 | |||
3c228bf5c3 | |||
8a80f07db2 | |||
ae59a3e576 | |||
df21e678d6 | |||
a71532b3e3 | |||
d7cb0b3ea1 | |||
ba8f137885 | |||
958ff66070 | |||
ad57c66a32 | |||
2bba0ddd74 |
@ -44,6 +44,7 @@ class RadiusProviderSerializer(ProviderSerializer):
|
|||||||
"shared_secret",
|
"shared_secret",
|
||||||
"outpost_set",
|
"outpost_set",
|
||||||
"mfa_support",
|
"mfa_support",
|
||||||
|
"certificate",
|
||||||
]
|
]
|
||||||
extra_kwargs = ProviderSerializer.Meta.extra_kwargs
|
extra_kwargs = ProviderSerializer.Meta.extra_kwargs
|
||||||
|
|
||||||
@ -79,6 +80,7 @@ class RadiusOutpostConfigSerializer(ModelSerializer):
|
|||||||
"client_networks",
|
"client_networks",
|
||||||
"shared_secret",
|
"shared_secret",
|
||||||
"mfa_support",
|
"mfa_support",
|
||||||
|
"certificate",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@ -0,0 +1,25 @@
|
|||||||
|
# Generated by Django 5.1.9 on 2025-05-16 13:53
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
("authentik_crypto", "0004_alter_certificatekeypair_name"),
|
||||||
|
("authentik_providers_radius", "0004_alter_radiusproviderpropertymapping_options"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name="radiusprovider",
|
||||||
|
name="certificate",
|
||||||
|
field=models.ForeignKey(
|
||||||
|
default=None,
|
||||||
|
null=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
to="authentik_crypto.certificatekeypair",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
]
|
@ -1,11 +1,14 @@
|
|||||||
"""Radius Provider"""
|
"""Radius Provider"""
|
||||||
|
|
||||||
|
from collections.abc import Iterable
|
||||||
|
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.templatetags.static import static
|
from django.templatetags.static import static
|
||||||
from django.utils.translation import gettext_lazy as _
|
from django.utils.translation import gettext_lazy as _
|
||||||
from rest_framework.serializers import Serializer
|
from rest_framework.serializers import Serializer
|
||||||
|
|
||||||
from authentik.core.models import PropertyMapping, Provider
|
from authentik.core.models import PropertyMapping, Provider
|
||||||
|
from authentik.crypto.models import CertificateKeyPair
|
||||||
from authentik.lib.generators import generate_id
|
from authentik.lib.generators import generate_id
|
||||||
from authentik.outposts.models import OutpostModel
|
from authentik.outposts.models import OutpostModel
|
||||||
|
|
||||||
@ -38,6 +41,10 @@ class RadiusProvider(OutpostModel, Provider):
|
|||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
certificate = models.ForeignKey(
|
||||||
|
CertificateKeyPair, on_delete=models.CASCADE, default=None, null=True
|
||||||
|
)
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def launch_url(self) -> str | None:
|
def launch_url(self) -> str | None:
|
||||||
"""Radius never has a launch URL"""
|
"""Radius never has a launch URL"""
|
||||||
@ -57,6 +64,12 @@ class RadiusProvider(OutpostModel, Provider):
|
|||||||
|
|
||||||
return RadiusProviderSerializer
|
return RadiusProviderSerializer
|
||||||
|
|
||||||
|
def get_required_objects(self) -> Iterable[models.Model | str]:
|
||||||
|
required_models = [self, "authentik_stages_mtls.pass_outpost_certificate"]
|
||||||
|
if self.certificate is not None:
|
||||||
|
required_models.append(self.certificate)
|
||||||
|
return required_models
|
||||||
|
|
||||||
def __str__(self):
|
def __str__(self):
|
||||||
return f"Radius Provider {self.name}"
|
return f"Radius Provider {self.name}"
|
||||||
|
|
||||||
|
@ -8953,6 +8953,11 @@
|
|||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"title": "MFA Support",
|
"title": "MFA Support",
|
||||||
"description": "When enabled, code-based multi-factor authentication can be used by appending a semicolon and the TOTP code to the password. This should only be enabled if all users that will bind to this provider have a TOTP device configured, as otherwise a password may incorrectly be rejected if it contains a semicolon."
|
"description": "When enabled, code-based multi-factor authentication can be used by appending a semicolon and the TOTP code to the password. This should only be enabled if all users that will bind to this provider have a TOTP device configured, as otherwise a password may incorrectly be rejected if it contains a semicolon."
|
||||||
|
},
|
||||||
|
"certificate": {
|
||||||
|
"type": "string",
|
||||||
|
"format": "uuid",
|
||||||
|
"title": "Certificate"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": []
|
"required": []
|
||||||
|
@ -34,9 +34,10 @@ var (
|
|||||||
type SolverFunction func(*api.ChallengeTypes, api.ApiFlowsExecutorSolveRequest) (api.FlowChallengeResponseRequest, error)
|
type SolverFunction func(*api.ChallengeTypes, api.ApiFlowsExecutorSolveRequest) (api.FlowChallengeResponseRequest, error)
|
||||||
|
|
||||||
type FlowExecutor struct {
|
type FlowExecutor struct {
|
||||||
Params url.Values
|
Params url.Values
|
||||||
Answers map[StageComponent]string
|
Answers map[StageComponent]string
|
||||||
Context context.Context
|
Context context.Context
|
||||||
|
InteractiveSolver SolverFunction
|
||||||
|
|
||||||
solvers map[StageComponent]SolverFunction
|
solvers map[StageComponent]SolverFunction
|
||||||
|
|
||||||
@ -94,6 +95,10 @@ func NewFlowExecutor(ctx context.Context, flowSlug string, refConfig *api.Config
|
|||||||
return fe
|
return fe
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (fe *FlowExecutor) AddHeader(name string, value string) {
|
||||||
|
fe.api.GetConfig().AddDefaultHeader(name, value)
|
||||||
|
}
|
||||||
|
|
||||||
func (fe *FlowExecutor) RoundTrip(req *http.Request) (*http.Response, error) {
|
func (fe *FlowExecutor) RoundTrip(req *http.Request) (*http.Response, error) {
|
||||||
res, err := fe.transport.RoundTrip(req)
|
res, err := fe.transport.RoundTrip(req)
|
||||||
if res != nil {
|
if res != nil {
|
||||||
@ -110,7 +115,7 @@ func (fe *FlowExecutor) ApiClient() *api.APIClient {
|
|||||||
return fe.api
|
return fe.api
|
||||||
}
|
}
|
||||||
|
|
||||||
type challengeCommon interface {
|
type ChallengeCommon interface {
|
||||||
GetComponent() string
|
GetComponent() string
|
||||||
GetResponseErrors() map[string][]api.ErrorDetail
|
GetResponseErrors() map[string][]api.ErrorDetail
|
||||||
}
|
}
|
||||||
@ -165,7 +170,7 @@ func (fe *FlowExecutor) getInitialChallenge() (*api.ChallengeTypes, error) {
|
|||||||
if i == nil {
|
if i == nil {
|
||||||
return nil, errors.New("response instance was null")
|
return nil, errors.New("response instance was null")
|
||||||
}
|
}
|
||||||
ch := i.(challengeCommon)
|
ch := i.(ChallengeCommon)
|
||||||
fe.log.WithField("component", ch.GetComponent()).Debug("Got challenge")
|
fe.log.WithField("component", ch.GetComponent()).Debug("Got challenge")
|
||||||
gcsp.SetTag("authentik.flow.component", ch.GetComponent())
|
gcsp.SetTag("authentik.flow.component", ch.GetComponent())
|
||||||
gcsp.Finish()
|
gcsp.Finish()
|
||||||
@ -184,7 +189,7 @@ func (fe *FlowExecutor) solveFlowChallenge(challenge *api.ChallengeTypes, depth
|
|||||||
if i == nil {
|
if i == nil {
|
||||||
return false, errors.New("response request instance was null")
|
return false, errors.New("response request instance was null")
|
||||||
}
|
}
|
||||||
ch := i.(challengeCommon)
|
ch := i.(ChallengeCommon)
|
||||||
|
|
||||||
// Check for any validation errors that we might've gotten
|
// Check for any validation errors that we might've gotten
|
||||||
if len(ch.GetResponseErrors()) > 0 {
|
if len(ch.GetResponseErrors()) > 0 {
|
||||||
@ -201,11 +206,17 @@ func (fe *FlowExecutor) solveFlowChallenge(challenge *api.ChallengeTypes, depth
|
|||||||
case string(StageRedirect):
|
case string(StageRedirect):
|
||||||
return true, nil
|
return true, nil
|
||||||
default:
|
default:
|
||||||
solver, ok := fe.solvers[StageComponent(ch.GetComponent())]
|
var err error
|
||||||
if !ok {
|
var rr api.FlowChallengeResponseRequest
|
||||||
return false, fmt.Errorf("unsupported challenge type %s", ch.GetComponent())
|
if fe.InteractiveSolver != nil {
|
||||||
|
rr, err = fe.InteractiveSolver(challenge, responseReq)
|
||||||
|
} else {
|
||||||
|
solver, ok := fe.solvers[StageComponent(ch.GetComponent())]
|
||||||
|
if !ok {
|
||||||
|
return false, fmt.Errorf("unsupported challenge type %s", ch.GetComponent())
|
||||||
|
}
|
||||||
|
rr, err = solver(challenge, responseReq)
|
||||||
}
|
}
|
||||||
rr, err := solver(challenge, responseReq)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
@ -220,7 +231,7 @@ func (fe *FlowExecutor) solveFlowChallenge(challenge *api.ChallengeTypes, depth
|
|||||||
if i == nil {
|
if i == nil {
|
||||||
return false, errors.New("response instance was null")
|
return false, errors.New("response instance was null")
|
||||||
}
|
}
|
||||||
ch = i.(challengeCommon)
|
ch = i.(ChallengeCommon)
|
||||||
fe.log.WithField("component", ch.GetComponent()).Debug("Got response")
|
fe.log.WithField("component", ch.GetComponent()).Debug("Got response")
|
||||||
scsp.SetTag("authentik.flow.component", ch.GetComponent())
|
scsp.SetTag("authentik.flow.component", ch.GetComponent())
|
||||||
scsp.Finish()
|
scsp.Finish()
|
||||||
|
@ -8,6 +8,6 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func TestConvert(t *testing.T) {
|
func TestConvert(t *testing.T) {
|
||||||
var a challengeCommon = api.NewIdentificationChallengeWithDefaults()
|
var a ChallengeCommon = api.NewIdentificationChallengeWithDefaults()
|
||||||
assert.NotNil(t, a)
|
assert.NotNil(t, a)
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
|
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"goauthentik.io/internal/outpost/ak"
|
"goauthentik.io/internal/outpost/ak"
|
||||||
|
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||||
)
|
)
|
||||||
|
|
||||||
func parseCIDRs(raw string) []*net.IPNet {
|
func parseCIDRs(raw string) []*net.IPNet {
|
||||||
@ -41,26 +42,28 @@ func (rs *RadiusServer) Refresh() error {
|
|||||||
if len(apiProviders) < 1 {
|
if len(apiProviders) < 1 {
|
||||||
return errors.New("no radius provider defined")
|
return errors.New("no radius provider defined")
|
||||||
}
|
}
|
||||||
providers := make([]*ProviderInstance, len(apiProviders))
|
providers := make(map[int32]*ProviderInstance)
|
||||||
for idx, provider := range apiProviders {
|
for _, provider := range apiProviders {
|
||||||
|
existing, ok := rs.providers[provider.Pk]
|
||||||
|
state := map[string]*protocol.State{}
|
||||||
|
if ok {
|
||||||
|
state = existing.eapState
|
||||||
|
}
|
||||||
logger := log.WithField("logger", "authentik.outpost.radius").WithField("provider", provider.Name)
|
logger := log.WithField("logger", "authentik.outpost.radius").WithField("provider", provider.Name)
|
||||||
providers[idx] = &ProviderInstance{
|
providers[provider.Pk] = &ProviderInstance{
|
||||||
SharedSecret: []byte(provider.GetSharedSecret()),
|
SharedSecret: []byte(provider.GetSharedSecret()),
|
||||||
ClientNetworks: parseCIDRs(provider.GetClientNetworks()),
|
ClientNetworks: parseCIDRs(provider.GetClientNetworks()),
|
||||||
MFASupport: provider.GetMfaSupport(),
|
MFASupport: provider.GetMfaSupport(),
|
||||||
appSlug: provider.ApplicationSlug,
|
appSlug: provider.ApplicationSlug,
|
||||||
flowSlug: provider.AuthFlowSlug,
|
flowSlug: provider.AuthFlowSlug,
|
||||||
|
certId: provider.GetCertificate(),
|
||||||
providerId: provider.Pk,
|
providerId: provider.Pk,
|
||||||
s: rs,
|
s: rs,
|
||||||
log: logger,
|
log: logger,
|
||||||
|
eapState: state,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
rs.providers = providers
|
rs.providers = providers
|
||||||
rs.log.Info("Update providers")
|
rs.log.Info("Update providers")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (rs *RadiusServer) StartRadiusServer() error {
|
|
||||||
rs.log.WithField("listen", rs.s.Addr).Info("Starting radius server")
|
|
||||||
return rs.s.ListenAndServe()
|
|
||||||
}
|
|
||||||
|
44
internal/outpost/radius/eap/README.md
Normal file
44
internal/outpost/radius/eap/README.md
Normal file
@ -0,0 +1,44 @@
|
|||||||
|
# EAP protocol implementation
|
||||||
|
|
||||||
|
Install `eapol_test` (`sudo apt install eapoltest`)
|
||||||
|
|
||||||
|
Both PEAP and EAP-TLS require a minimal PKI setup. A CA, a certificate for the server and for EAP-TLS a client certificate need to be provided.
|
||||||
|
|
||||||
|
Save either of the config files below and run eapoltest like so:
|
||||||
|
|
||||||
|
```
|
||||||
|
# peap.conf is the config file under the PEAP testing section
|
||||||
|
# foo is the shared RADIUS secret
|
||||||
|
# 1.2.3.4 is the IP of the RADIUS server
|
||||||
|
eapol_test -c peap.conf -s foo -a 1.2.3.4
|
||||||
|
```
|
||||||
|
|
||||||
|
### PEAP testing
|
||||||
|
|
||||||
|
```
|
||||||
|
network={
|
||||||
|
ssid="DoesNotMatterForThisTest"
|
||||||
|
key_mgmt=WPA-EAP
|
||||||
|
eap=PEAP
|
||||||
|
identity="foo"
|
||||||
|
password="bar"
|
||||||
|
ca_cert="ca.pem"
|
||||||
|
phase2="auth=MSCHAPV2"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### EAP-TLS testing
|
||||||
|
|
||||||
|
```
|
||||||
|
network={
|
||||||
|
ssid="DoesNotMatterForThisTest"
|
||||||
|
key_mgmt=WPA-EAP
|
||||||
|
eap=TLS
|
||||||
|
identity="foo"
|
||||||
|
ca_cert="ca.pem"
|
||||||
|
client_cert="cert_client.pem"
|
||||||
|
private_key="cert_client.key"
|
||||||
|
eapol_flags=3
|
||||||
|
eap_workaround=0
|
||||||
|
}
|
||||||
|
```
|
55
internal/outpost/radius/eap/context.go
Normal file
55
internal/outpost/radius/eap/context.go
Normal file
@ -0,0 +1,55 @@
|
|||||||
|
package eap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||||
|
"layeh.com/radius"
|
||||||
|
)
|
||||||
|
|
||||||
|
type context struct {
|
||||||
|
req *radius.Request
|
||||||
|
rootPayload protocol.Payload
|
||||||
|
typeState map[protocol.Type]any
|
||||||
|
log *log.Entry
|
||||||
|
settings interface{}
|
||||||
|
parent *context
|
||||||
|
endStatus protocol.Status
|
||||||
|
handleInner func(protocol.Payload, protocol.StateManager, protocol.Context) (protocol.Payload, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ctx *context) RootPayload() protocol.Payload { return ctx.rootPayload }
|
||||||
|
func (ctx *context) Packet() *radius.Request { return ctx.req }
|
||||||
|
func (ctx *context) ProtocolSettings() any { return ctx.settings }
|
||||||
|
func (ctx *context) GetProtocolState(p protocol.Type) any { return ctx.typeState[p] }
|
||||||
|
func (ctx *context) SetProtocolState(p protocol.Type, st any) { ctx.typeState[p] = st }
|
||||||
|
func (ctx *context) IsProtocolStart(p protocol.Type) bool { return ctx.typeState[p] == nil }
|
||||||
|
func (ctx *context) Log() *log.Entry { return ctx.log }
|
||||||
|
func (ctx *context) HandleInnerEAP(p protocol.Payload, st protocol.StateManager) (protocol.Payload, error) {
|
||||||
|
return ctx.handleInner(p, st, ctx)
|
||||||
|
}
|
||||||
|
func (ctx *context) Inner(p protocol.Payload, t protocol.Type) protocol.Context {
|
||||||
|
nctx := &context{
|
||||||
|
req: ctx.req,
|
||||||
|
rootPayload: ctx.rootPayload,
|
||||||
|
typeState: ctx.typeState,
|
||||||
|
log: ctx.log.WithField("type", fmt.Sprintf("%T", p)).WithField("code", t),
|
||||||
|
settings: ctx.settings,
|
||||||
|
parent: ctx,
|
||||||
|
handleInner: ctx.handleInner,
|
||||||
|
}
|
||||||
|
nctx.log.Debug("Creating inner context")
|
||||||
|
return nctx
|
||||||
|
}
|
||||||
|
func (ctx *context) EndInnerProtocol(st protocol.Status) {
|
||||||
|
ctx.log.Info("Ending protocol")
|
||||||
|
if ctx.parent != nil {
|
||||||
|
ctx.parent.EndInnerProtocol(st)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if ctx.endStatus != protocol.StatusUnknown {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.endStatus = st
|
||||||
|
}
|
13
internal/outpost/radius/eap/debug/debug.go
Normal file
13
internal/outpost/radius/eap/debug/debug.go
Normal file
@ -0,0 +1,13 @@
|
|||||||
|
package debug
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
func FormatBytes(d []byte) string {
|
||||||
|
b := d
|
||||||
|
if len(b) > 32 {
|
||||||
|
b = b[:32]
|
||||||
|
}
|
||||||
|
return fmt.Sprintf("% x", b)
|
||||||
|
}
|
182
internal/outpost/radius/eap/handler.go
Normal file
182
internal/outpost/radius/eap/handler.go
Normal file
@ -0,0 +1,182 @@
|
|||||||
|
package eap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/hmac"
|
||||||
|
"crypto/md5"
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"reflect"
|
||||||
|
|
||||||
|
"github.com/gorilla/securecookie"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||||
|
"goauthentik.io/internal/outpost/radius/eap/protocol/eap"
|
||||||
|
"goauthentik.io/internal/outpost/radius/eap/protocol/legacy_nak"
|
||||||
|
"layeh.com/radius"
|
||||||
|
"layeh.com/radius/rfc2865"
|
||||||
|
"layeh.com/radius/rfc2869"
|
||||||
|
)
|
||||||
|
|
||||||
|
func sendErrorResponse(w radius.ResponseWriter, r *radius.Request) {
|
||||||
|
rres := r.Response(radius.CodeAccessReject)
|
||||||
|
err := w.Write(rres)
|
||||||
|
if err != nil {
|
||||||
|
log.WithError(err).Warning("failed to send response")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Packet) HandleRadiusPacket(w radius.ResponseWriter, r *radius.Request) {
|
||||||
|
p.r = r
|
||||||
|
rst := rfc2865.State_GetString(r.Packet)
|
||||||
|
if rst == "" {
|
||||||
|
rst = base64.StdEncoding.EncodeToString(securecookie.GenerateRandomKey(12))
|
||||||
|
}
|
||||||
|
p.state = rst
|
||||||
|
|
||||||
|
rp := &Packet{r: r}
|
||||||
|
rep, err := p.handleEAP(p.eap, p.stm, nil)
|
||||||
|
rp.eap = rep
|
||||||
|
|
||||||
|
rres := r.Response(radius.CodeAccessReject)
|
||||||
|
if err == nil {
|
||||||
|
switch rp.eap.Code {
|
||||||
|
case protocol.CodeRequest:
|
||||||
|
rres.Code = radius.CodeAccessChallenge
|
||||||
|
case protocol.CodeFailure:
|
||||||
|
rres.Code = radius.CodeAccessReject
|
||||||
|
case protocol.CodeSuccess:
|
||||||
|
rres.Code = radius.CodeAccessAccept
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
rres.Code = radius.CodeAccessReject
|
||||||
|
log.WithError(err).Debug("Rejecting request")
|
||||||
|
}
|
||||||
|
for _, mod := range p.responseModifiers {
|
||||||
|
err := mod.ModifyRADIUSResponse(rres, r.Packet)
|
||||||
|
if err != nil {
|
||||||
|
log.WithError(err).Warning("Root-EAP: failed to modify response packet")
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
rfc2865.State_SetString(rres, p.state)
|
||||||
|
eapEncoded, err := rp.Encode()
|
||||||
|
if err != nil {
|
||||||
|
log.WithError(err).Warning("failed to encode response")
|
||||||
|
sendErrorResponse(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.WithField("length", len(eapEncoded)).WithField("type", fmt.Sprintf("%T", rp.eap.Payload)).Debug("Root-EAP: encapsulated challenge")
|
||||||
|
rfc2869.EAPMessage_Set(rres, eapEncoded)
|
||||||
|
err = p.setMessageAuthenticator(rres)
|
||||||
|
if err != nil {
|
||||||
|
log.WithError(err).Warning("failed to send message authenticator")
|
||||||
|
sendErrorResponse(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = w.Write(rres)
|
||||||
|
if err != nil {
|
||||||
|
log.WithError(err).Warning("failed to send response")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Packet) handleEAP(pp protocol.Payload, stm protocol.StateManager, parentContext *context) (*eap.Payload, error) {
|
||||||
|
st := stm.GetEAPState(p.state)
|
||||||
|
if st == nil {
|
||||||
|
log.Debug("Root-EAP: blank state")
|
||||||
|
st = protocol.BlankState(stm.GetEAPSettings())
|
||||||
|
}
|
||||||
|
|
||||||
|
nextChallengeToOffer, err := st.GetNextProtocol()
|
||||||
|
if err != nil {
|
||||||
|
return &eap.Payload{
|
||||||
|
Code: protocol.CodeFailure,
|
||||||
|
ID: p.eap.ID,
|
||||||
|
}, err
|
||||||
|
}
|
||||||
|
|
||||||
|
next := func() (*eap.Payload, error) {
|
||||||
|
st.ProtocolIndex += 1
|
||||||
|
st.TypeState = map[protocol.Type]any{}
|
||||||
|
stm.SetEAPState(p.state, st)
|
||||||
|
return p.handleEAP(pp, stm, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
if n, ok := pp.(*eap.Payload).Payload.(*legacy_nak.Payload); ok {
|
||||||
|
log.WithField("desired", n.DesiredType).Debug("Root-EAP: received NAK, trying next protocol")
|
||||||
|
pp.(*eap.Payload).Payload = nil
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
|
||||||
|
np, t, _ := eap.EmptyPayload(stm.GetEAPSettings(), nextChallengeToOffer)
|
||||||
|
|
||||||
|
var ctx *context
|
||||||
|
if parentContext != nil {
|
||||||
|
ctx = parentContext.Inner(np, t).(*context)
|
||||||
|
ctx.settings = stm.GetEAPSettings().ProtocolSettings[np.Type()]
|
||||||
|
} else {
|
||||||
|
ctx = &context{
|
||||||
|
req: p.r,
|
||||||
|
rootPayload: p.eap,
|
||||||
|
typeState: st.TypeState,
|
||||||
|
log: log.WithField("type", fmt.Sprintf("%T", np)).WithField("code", t),
|
||||||
|
settings: stm.GetEAPSettings().ProtocolSettings[t],
|
||||||
|
}
|
||||||
|
ctx.handleInner = func(pp protocol.Payload, sm protocol.StateManager, ctx protocol.Context) (protocol.Payload, error) {
|
||||||
|
// cctx := ctx.Inner(np, np.Type(), nil).(*context)
|
||||||
|
return p.handleEAP(pp, sm, ctx.(*context))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !np.Offerable() {
|
||||||
|
ctx.Log().Debug("Root-EAP: protocol not offerable, skipping")
|
||||||
|
return next()
|
||||||
|
}
|
||||||
|
ctx.Log().Debug("Root-EAP: Passing to protocol")
|
||||||
|
|
||||||
|
res := &eap.Payload{
|
||||||
|
Code: protocol.CodeRequest,
|
||||||
|
ID: p.eap.ID + 1,
|
||||||
|
MsgType: t,
|
||||||
|
}
|
||||||
|
var payload any
|
||||||
|
if reflect.TypeOf(pp.(*eap.Payload).Payload) == reflect.TypeOf(np) {
|
||||||
|
np.Decode(pp.(*eap.Payload).RawPayload)
|
||||||
|
}
|
||||||
|
payload = np.Handle(ctx)
|
||||||
|
if payload != nil {
|
||||||
|
res.Payload = payload.(protocol.Payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
stm.SetEAPState(p.state, st)
|
||||||
|
|
||||||
|
if rm, ok := np.(protocol.ResponseModifier); ok {
|
||||||
|
ctx.log.Debug("Root-EAP: Registered response modifier")
|
||||||
|
p.responseModifiers = append(p.responseModifiers, rm)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch ctx.endStatus {
|
||||||
|
case protocol.StatusSuccess:
|
||||||
|
res.Code = protocol.CodeSuccess
|
||||||
|
res.ID -= 1
|
||||||
|
case protocol.StatusError:
|
||||||
|
res.Code = protocol.CodeFailure
|
||||||
|
res.ID -= 1
|
||||||
|
case protocol.StatusNextProtocol:
|
||||||
|
ctx.log.Debug("Root-EAP: Protocol ended, starting next protocol")
|
||||||
|
return next()
|
||||||
|
case protocol.StatusUnknown:
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Packet) setMessageAuthenticator(rp *radius.Packet) error {
|
||||||
|
_ = rfc2869.MessageAuthenticator_Set(rp, make([]byte, 16))
|
||||||
|
hash := hmac.New(md5.New, rp.Secret)
|
||||||
|
encode, err := rp.MarshalBinary()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
hash.Write(encode)
|
||||||
|
_ = rfc2869.MessageAuthenticator_Set(rp, hash.Sum(nil))
|
||||||
|
return nil
|
||||||
|
}
|
34
internal/outpost/radius/eap/packet.go
Normal file
34
internal/outpost/radius/eap/packet.go
Normal file
@ -0,0 +1,34 @@
|
|||||||
|
package eap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||||
|
"goauthentik.io/internal/outpost/radius/eap/protocol/eap"
|
||||||
|
"layeh.com/radius"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Packet struct {
|
||||||
|
r *radius.Request
|
||||||
|
eap *eap.Payload
|
||||||
|
stm protocol.StateManager
|
||||||
|
state string
|
||||||
|
responseModifiers []protocol.ResponseModifier
|
||||||
|
}
|
||||||
|
|
||||||
|
func Decode(stm protocol.StateManager, raw []byte) (*Packet, error) {
|
||||||
|
packet := &Packet{
|
||||||
|
eap: &eap.Payload{
|
||||||
|
Settings: stm.GetEAPSettings(),
|
||||||
|
},
|
||||||
|
stm: stm,
|
||||||
|
responseModifiers: []protocol.ResponseModifier{},
|
||||||
|
}
|
||||||
|
err := packet.eap.Decode(raw)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return packet, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Packet) Encode() ([]byte, error) {
|
||||||
|
return p.eap.Encode()
|
||||||
|
}
|
32
internal/outpost/radius/eap/protocol/context.go
Normal file
32
internal/outpost/radius/eap/protocol/context.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package protocol
|
||||||
|
|
||||||
|
import (
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"layeh.com/radius"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Status int
|
||||||
|
|
||||||
|
const (
|
||||||
|
StatusUnknown Status = iota
|
||||||
|
StatusSuccess
|
||||||
|
StatusError
|
||||||
|
StatusNextProtocol
|
||||||
|
)
|
||||||
|
|
||||||
|
type Context interface {
|
||||||
|
Packet() *radius.Request
|
||||||
|
RootPayload() Payload
|
||||||
|
|
||||||
|
ProtocolSettings() interface{}
|
||||||
|
|
||||||
|
GetProtocolState(p Type) interface{}
|
||||||
|
SetProtocolState(p Type, s interface{})
|
||||||
|
IsProtocolStart(p Type) bool
|
||||||
|
|
||||||
|
HandleInnerEAP(Payload, StateManager) (Payload, error)
|
||||||
|
Inner(Payload, Type) Context
|
||||||
|
EndInnerProtocol(Status)
|
||||||
|
|
||||||
|
Log() *log.Entry
|
||||||
|
}
|
23
internal/outpost/radius/eap/protocol/eap/decode.go
Normal file
23
internal/outpost/radius/eap/protocol/eap/decode.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package eap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||||
|
)
|
||||||
|
|
||||||
|
func EmptyPayload(settings protocol.Settings, t protocol.Type) (protocol.Payload, protocol.Type, error) {
|
||||||
|
for _, cons := range settings.Protocols {
|
||||||
|
np := cons()
|
||||||
|
if np.Type() == t {
|
||||||
|
return np, np.Type(), nil
|
||||||
|
}
|
||||||
|
// If the protocol has an inner protocol, return the original type but the code for the inner protocol
|
||||||
|
if i, ok := np.(protocol.Inner); ok {
|
||||||
|
if ii := i.HasInner(); ii != nil {
|
||||||
|
return np, ii.Type(), nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, protocol.Type(0), fmt.Errorf("unsupported EAP type %d", t)
|
||||||
|
}
|
96
internal/outpost/radius/eap/protocol/eap/payload.go
Normal file
96
internal/outpost/radius/eap/protocol/eap/payload.go
Normal file
@ -0,0 +1,96 @@
|
|||||||
|
package eap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"goauthentik.io/internal/outpost/radius/eap/debug"
|
||||||
|
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||||
|
)
|
||||||
|
|
||||||
|
const TypeEAP protocol.Type = 0
|
||||||
|
|
||||||
|
func Protocol() protocol.Payload {
|
||||||
|
return &Payload{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Payload struct {
|
||||||
|
Code protocol.Code
|
||||||
|
ID uint8
|
||||||
|
Length uint16
|
||||||
|
MsgType protocol.Type
|
||||||
|
Payload protocol.Payload
|
||||||
|
RawPayload []byte
|
||||||
|
|
||||||
|
Settings protocol.Settings
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payload) Type() protocol.Type {
|
||||||
|
return TypeEAP
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payload) Offerable() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payload) Decode(raw []byte) error {
|
||||||
|
p.Code = protocol.Code(raw[0])
|
||||||
|
p.ID = raw[1]
|
||||||
|
p.Length = binary.BigEndian.Uint16(raw[2:])
|
||||||
|
if p.Length != uint16(len(raw)) {
|
||||||
|
return fmt.Errorf("mismatched packet length; got %d, expected %d", p.Length, uint16(len(raw)))
|
||||||
|
}
|
||||||
|
if len(raw) > 4 && (p.Code == protocol.CodeRequest || p.Code == protocol.CodeResponse) {
|
||||||
|
p.MsgType = protocol.Type(raw[4])
|
||||||
|
}
|
||||||
|
log.WithField("raw", debug.FormatBytes(raw)).Trace("EAP: decode raw")
|
||||||
|
p.RawPayload = raw[5:]
|
||||||
|
if p.Payload == nil {
|
||||||
|
pp, _, err := EmptyPayload(p.Settings, p.MsgType)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
p.Payload = pp
|
||||||
|
}
|
||||||
|
err := p.Payload.Decode(raw[5:])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payload) Encode() ([]byte, error) {
|
||||||
|
buff := make([]byte, 4)
|
||||||
|
buff[0] = uint8(p.Code)
|
||||||
|
buff[1] = uint8(p.ID)
|
||||||
|
|
||||||
|
if p.Payload != nil {
|
||||||
|
payloadBuffer, err := p.Payload.Encode()
|
||||||
|
if err != nil {
|
||||||
|
return buff, err
|
||||||
|
}
|
||||||
|
if p.Code == protocol.CodeRequest || p.Code == protocol.CodeResponse {
|
||||||
|
buff = append(buff, uint8(p.MsgType))
|
||||||
|
}
|
||||||
|
buff = append(buff, payloadBuffer...)
|
||||||
|
}
|
||||||
|
binary.BigEndian.PutUint16(buff[2:], uint16(len(buff)))
|
||||||
|
return buff, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payload) Handle(ctx protocol.Context) protocol.Payload {
|
||||||
|
ctx.Log().Debug("EAP: Handle")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payload) String() string {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"<EAP Packet Code=%d, ID=%d, Type=%d, Length=%d, Payload=%T>",
|
||||||
|
p.Code,
|
||||||
|
p.ID,
|
||||||
|
p.MsgType,
|
||||||
|
p.Length,
|
||||||
|
p.Payload,
|
||||||
|
)
|
||||||
|
}
|
5
internal/outpost/radius/eap/protocol/eap/state.go
Normal file
5
internal/outpost/radius/eap/protocol/eap/state.go
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
package eap
|
||||||
|
|
||||||
|
type State struct {
|
||||||
|
PacketID uint8
|
||||||
|
}
|
61
internal/outpost/radius/eap/protocol/gtc/payload.go
Normal file
61
internal/outpost/radius/eap/protocol/gtc/payload.go
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
package gtc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||||
|
)
|
||||||
|
|
||||||
|
const TypeGTC protocol.Type = 6
|
||||||
|
|
||||||
|
func Protocol() protocol.Payload {
|
||||||
|
return &Payload{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Payload struct {
|
||||||
|
Challenge []byte
|
||||||
|
|
||||||
|
st *State
|
||||||
|
raw []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payload) Type() protocol.Type {
|
||||||
|
return TypeGTC
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payload) Decode(raw []byte) error {
|
||||||
|
p.raw = raw
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payload) Encode() ([]byte, error) {
|
||||||
|
return p.Challenge, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payload) Handle(ctx protocol.Context) protocol.Payload {
|
||||||
|
defer func() {
|
||||||
|
ctx.SetProtocolState(TypeGTC, p.st)
|
||||||
|
}()
|
||||||
|
settings := ctx.ProtocolSettings().(Settings)
|
||||||
|
if ctx.IsProtocolStart(TypeGTC) {
|
||||||
|
g, v := settings.ChallengeHandler(ctx)
|
||||||
|
p.st = &State{
|
||||||
|
getChallenge: g,
|
||||||
|
validateResponse: v,
|
||||||
|
}
|
||||||
|
return &Payload{
|
||||||
|
Challenge: p.st.getChallenge(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.st = ctx.GetProtocolState(TypeGTC).(*State)
|
||||||
|
p.st.validateResponse(p.raw)
|
||||||
|
return &Payload{
|
||||||
|
Challenge: p.st.getChallenge(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payload) Offerable() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payload) String() string {
|
||||||
|
return "<GTC Packet>"
|
||||||
|
}
|
10
internal/outpost/radius/eap/protocol/gtc/settings.go
Normal file
10
internal/outpost/radius/eap/protocol/gtc/settings.go
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package gtc
|
||||||
|
|
||||||
|
import "goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||||
|
|
||||||
|
type GetChallenge func() []byte
|
||||||
|
type ValidateResponse func(answer []byte)
|
||||||
|
|
||||||
|
type Settings struct {
|
||||||
|
ChallengeHandler func(ctx protocol.Context) (GetChallenge, ValidateResponse)
|
||||||
|
}
|
6
internal/outpost/radius/eap/protocol/gtc/state.go
Normal file
6
internal/outpost/radius/eap/protocol/gtc/state.go
Normal file
@ -0,0 +1,6 @@
|
|||||||
|
package gtc
|
||||||
|
|
||||||
|
type State struct {
|
||||||
|
getChallenge GetChallenge
|
||||||
|
validateResponse ValidateResponse
|
||||||
|
}
|
48
internal/outpost/radius/eap/protocol/identity/payload.go
Normal file
48
internal/outpost/radius/eap/protocol/identity/payload.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
package identity
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||||
|
)
|
||||||
|
|
||||||
|
const TypeIdentity protocol.Type = 1
|
||||||
|
|
||||||
|
func Protocol() protocol.Payload {
|
||||||
|
return &Payload{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Payload struct {
|
||||||
|
Identity string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payload) Type() protocol.Type {
|
||||||
|
return TypeIdentity
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payload) Decode(raw []byte) error {
|
||||||
|
p.Identity = string(raw)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payload) Encode() ([]byte, error) {
|
||||||
|
return []byte{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payload) Handle(ctx protocol.Context) protocol.Payload {
|
||||||
|
if ctx.IsProtocolStart(TypeIdentity) {
|
||||||
|
ctx.EndInnerProtocol(protocol.StatusNextProtocol)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payload) Offerable() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payload) String() string {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"<Identity Packet Identity=%s>",
|
||||||
|
p.Identity,
|
||||||
|
)
|
||||||
|
}
|
48
internal/outpost/radius/eap/protocol/legacy_nak/payload.go
Normal file
48
internal/outpost/radius/eap/protocol/legacy_nak/payload.go
Normal file
@ -0,0 +1,48 @@
|
|||||||
|
package legacy_nak
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||||
|
)
|
||||||
|
|
||||||
|
const TypeLegacyNAK protocol.Type = 3
|
||||||
|
|
||||||
|
func Protocol() protocol.Payload {
|
||||||
|
return &Payload{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Payload struct {
|
||||||
|
DesiredType protocol.Type
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payload) Type() protocol.Type {
|
||||||
|
return TypeLegacyNAK
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payload) Decode(raw []byte) error {
|
||||||
|
p.DesiredType = protocol.Type(raw[0])
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payload) Encode() ([]byte, error) {
|
||||||
|
return []byte{byte(p.DesiredType)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payload) Handle(ctx protocol.Context) protocol.Payload {
|
||||||
|
if ctx.IsProtocolStart(TypeLegacyNAK) {
|
||||||
|
ctx.EndInnerProtocol(protocol.StatusError)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payload) Offerable() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payload) String() string {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"<Legacy NAK Packet DesiredType=%d>",
|
||||||
|
p.DesiredType,
|
||||||
|
)
|
||||||
|
}
|
23
internal/outpost/radius/eap/protocol/mschapv2/op_response.go
Normal file
23
internal/outpost/radius/eap/protocol/mschapv2/op_response.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package mschapv2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"errors"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Response struct {
|
||||||
|
Challenge []byte
|
||||||
|
NTResponse []byte
|
||||||
|
Flags uint8
|
||||||
|
}
|
||||||
|
|
||||||
|
func ParseResponse(raw []byte) (*Response, error) {
|
||||||
|
res := &Response{}
|
||||||
|
res.Challenge = raw[:challengeValueSize]
|
||||||
|
if !bytes.Equal(raw[challengeValueSize:challengeValueSize+responseReservedSize], make([]byte, 8)) {
|
||||||
|
return nil, errors.New("MSCHAPv2: Reserved bytes not empty?")
|
||||||
|
}
|
||||||
|
res.NTResponse = raw[challengeValueSize+responseReservedSize : challengeValueSize+responseReservedSize+responseNTResponseSize]
|
||||||
|
res.Flags = (raw[challengeValueSize+responseReservedSize+responseNTResponseSize])
|
||||||
|
return res, nil
|
||||||
|
}
|
23
internal/outpost/radius/eap/protocol/mschapv2/op_success.go
Normal file
23
internal/outpost/radius/eap/protocol/mschapv2/op_success.go
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
package mschapv2
|
||||||
|
|
||||||
|
import "encoding/binary"
|
||||||
|
|
||||||
|
type SuccessRequest struct {
|
||||||
|
*Payload
|
||||||
|
Authenticator []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
// A success request is encoded slightly differently, it doesn't have a challenge and as such
|
||||||
|
// doesn't need to encode the length of it
|
||||||
|
func (sr *SuccessRequest) Encode() ([]byte, error) {
|
||||||
|
encoded := []byte{
|
||||||
|
byte(sr.OpCode),
|
||||||
|
sr.MSCHAPv2ID,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
}
|
||||||
|
encoded = append(encoded, sr.Authenticator...)
|
||||||
|
sr.MSLength = uint16(len(encoded))
|
||||||
|
binary.BigEndian.PutUint16(encoded[2:], sr.MSLength)
|
||||||
|
return encoded, nil
|
||||||
|
}
|
196
internal/outpost/radius/eap/protocol/mschapv2/payload.go
Normal file
196
internal/outpost/radius/eap/protocol/mschapv2/payload.go
Normal file
@ -0,0 +1,196 @@
|
|||||||
|
package mschapv2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/binary"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/gorilla/securecookie"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"goauthentik.io/internal/outpost/radius/eap/debug"
|
||||||
|
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||||
|
"goauthentik.io/internal/outpost/radius/eap/protocol/eap"
|
||||||
|
"goauthentik.io/internal/outpost/radius/eap/protocol/peap"
|
||||||
|
"layeh.com/radius"
|
||||||
|
"layeh.com/radius/vendors/microsoft"
|
||||||
|
)
|
||||||
|
|
||||||
|
const TypeMSCHAPv2 protocol.Type = 26
|
||||||
|
|
||||||
|
func Protocol() protocol.Payload {
|
||||||
|
return &Payload{}
|
||||||
|
}
|
||||||
|
|
||||||
|
const (
|
||||||
|
challengeValueSize = 16
|
||||||
|
responseValueSize = 49
|
||||||
|
responseReservedSize = 8
|
||||||
|
responseNTResponseSize = 24
|
||||||
|
)
|
||||||
|
|
||||||
|
type OpCode uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
OpChallenge OpCode = 1
|
||||||
|
OpResponse OpCode = 2
|
||||||
|
OpSuccess OpCode = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
type Payload struct {
|
||||||
|
OpCode OpCode
|
||||||
|
MSCHAPv2ID uint8
|
||||||
|
MSLength uint16
|
||||||
|
ValueSize uint8
|
||||||
|
|
||||||
|
Challenge []byte
|
||||||
|
Response []byte
|
||||||
|
|
||||||
|
Name []byte
|
||||||
|
|
||||||
|
st *State
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payload) Type() protocol.Type {
|
||||||
|
return TypeMSCHAPv2
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payload) Decode(raw []byte) error {
|
||||||
|
log.WithField("raw", debug.FormatBytes(raw)).Debugf("MSCHAPv2: decode raw")
|
||||||
|
p.OpCode = OpCode(raw[0])
|
||||||
|
if p.OpCode == OpSuccess {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// TODO: Validate against root EAP packet
|
||||||
|
p.MSCHAPv2ID = raw[1]
|
||||||
|
p.MSLength = binary.BigEndian.Uint16(raw[2:])
|
||||||
|
|
||||||
|
p.ValueSize = raw[4]
|
||||||
|
if p.ValueSize != responseValueSize {
|
||||||
|
return fmt.Errorf("MSCHAPv2: incorrect value size: %d", p.ValueSize)
|
||||||
|
}
|
||||||
|
p.Response = raw[5 : p.ValueSize+5]
|
||||||
|
p.Name = raw[5+p.ValueSize:]
|
||||||
|
if int(p.MSLength) != len(raw) {
|
||||||
|
return fmt.Errorf("MSCHAPv2: incorrect MS-Length: %d, should be %d", p.MSLength, len(raw))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payload) Encode() ([]byte, error) {
|
||||||
|
encoded := []byte{
|
||||||
|
byte(p.OpCode),
|
||||||
|
p.MSCHAPv2ID,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
byte(len(p.Challenge)),
|
||||||
|
}
|
||||||
|
encoded = append(encoded, p.Challenge...)
|
||||||
|
encoded = append(encoded, p.Name...)
|
||||||
|
p.MSLength = uint16(len(encoded))
|
||||||
|
binary.BigEndian.PutUint16(encoded[2:], p.MSLength)
|
||||||
|
return encoded, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payload) Handle(ctx protocol.Context) protocol.Payload {
|
||||||
|
defer func() {
|
||||||
|
ctx.SetProtocolState(TypeMSCHAPv2, p.st)
|
||||||
|
}()
|
||||||
|
|
||||||
|
rootEap := ctx.RootPayload().(*eap.Payload)
|
||||||
|
|
||||||
|
if ctx.IsProtocolStart(TypeMSCHAPv2) {
|
||||||
|
ctx.Log().Debug("MSCHAPv2: Empty state, starting")
|
||||||
|
p.st = &State{
|
||||||
|
Challenge: securecookie.GenerateRandomKey(challengeValueSize),
|
||||||
|
}
|
||||||
|
return &Payload{
|
||||||
|
OpCode: OpChallenge,
|
||||||
|
MSCHAPv2ID: rootEap.ID + 1,
|
||||||
|
Challenge: p.st.Challenge,
|
||||||
|
Name: []byte("authentik"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.st = ctx.GetProtocolState(TypeMSCHAPv2).(*State)
|
||||||
|
|
||||||
|
response := &Payload{
|
||||||
|
MSCHAPv2ID: rootEap.ID + 1,
|
||||||
|
}
|
||||||
|
|
||||||
|
settings := ctx.ProtocolSettings().(Settings)
|
||||||
|
|
||||||
|
ctx.Log().Debugf("MSCHAPv2: OpCode: %d", p.OpCode)
|
||||||
|
if p.OpCode == OpResponse {
|
||||||
|
res, err := ParseResponse(p.Response)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Log().WithError(err).Warning("MSCHAPv2: failed to parse response")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
p.st.PeerChallenge = res.Challenge
|
||||||
|
auth, err := settings.AuthenticateRequest(AuthRequest{
|
||||||
|
Challenge: p.st.Challenge,
|
||||||
|
PeerChallenge: p.st.PeerChallenge,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
ctx.Log().WithError(err).Warning("MSCHAPv2: failed to check password")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if !bytes.Equal(auth.NTResponse, res.NTResponse) {
|
||||||
|
ctx.Log().Warning("MSCHAPv2: NT response mismatch")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
ctx.Log().Info("MSCHAPv2: Successfully checked password")
|
||||||
|
p.st.AuthResponse = auth
|
||||||
|
succ := &SuccessRequest{
|
||||||
|
Payload: &Payload{
|
||||||
|
OpCode: OpSuccess,
|
||||||
|
},
|
||||||
|
Authenticator: []byte(auth.AuthenticatorResponse),
|
||||||
|
}
|
||||||
|
return succ
|
||||||
|
} else if p.OpCode == OpSuccess && p.st.AuthResponse != nil {
|
||||||
|
ep := &peap.ExtensionPayload{
|
||||||
|
AVPs: []peap.ExtensionAVP{
|
||||||
|
{
|
||||||
|
Mandatory: true,
|
||||||
|
Type: peap.AVPAckResult,
|
||||||
|
Value: []byte{0, 1},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
p.st.IsProtocolEnded = true
|
||||||
|
return ep
|
||||||
|
} else if p.st.IsProtocolEnded {
|
||||||
|
ctx.EndInnerProtocol(protocol.StatusSuccess)
|
||||||
|
return &Payload{}
|
||||||
|
}
|
||||||
|
return response
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payload) ModifyRADIUSResponse(r *radius.Packet, q *radius.Packet) error {
|
||||||
|
if p.st == nil || p.st.AuthResponse == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if r.Code != radius.CodeAccessAccept {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
log.Debug("MSCHAPv2: Radius modifier")
|
||||||
|
if len(microsoft.MSMPPERecvKey_Get(r, q)) < 1 {
|
||||||
|
microsoft.MSMPPERecvKey_Set(r, p.st.AuthResponse.RecvKey)
|
||||||
|
}
|
||||||
|
if len(microsoft.MSMPPESendKey_Get(r, q)) < 1 {
|
||||||
|
microsoft.MSMPPESendKey_Set(r, p.st.AuthResponse.SendKey)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payload) Offerable() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payload) String() string {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"<MSCHAPv2 Packet OpCode=%d, MSCHAPv2ID=%d>",
|
||||||
|
p.OpCode,
|
||||||
|
p.MSCHAPv2ID,
|
||||||
|
)
|
||||||
|
}
|
50
internal/outpost/radius/eap/protocol/mschapv2/settings.go
Normal file
50
internal/outpost/radius/eap/protocol/mschapv2/settings.go
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
package mschapv2
|
||||||
|
|
||||||
|
import (
|
||||||
|
"layeh.com/radius/rfc2759"
|
||||||
|
"layeh.com/radius/rfc3079"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Settings struct {
|
||||||
|
AuthenticateRequest func(req AuthRequest) (*AuthResponse, error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuthRequest struct {
|
||||||
|
Challenge []byte
|
||||||
|
PeerChallenge []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
type AuthResponse struct {
|
||||||
|
NTResponse []byte
|
||||||
|
RecvKey []byte
|
||||||
|
SendKey []byte
|
||||||
|
AuthenticatorResponse string
|
||||||
|
}
|
||||||
|
|
||||||
|
func DebugStaticCredentials(user, password []byte) func(req AuthRequest) (*AuthResponse, error) {
|
||||||
|
return func(req AuthRequest) (*AuthResponse, error) {
|
||||||
|
res := &AuthResponse{}
|
||||||
|
ntResponse, err := rfc2759.GenerateNTResponse(req.Challenge, req.PeerChallenge, user, password)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
res.NTResponse = ntResponse
|
||||||
|
|
||||||
|
res.RecvKey, err = rfc3079.MakeKey(ntResponse, password, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res.SendKey, err = rfc3079.MakeKey(ntResponse, password, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
res.AuthenticatorResponse, err = rfc2759.GenerateAuthenticatorResponse(req.Challenge, req.PeerChallenge, ntResponse, user, password)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return res, nil
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
8
internal/outpost/radius/eap/protocol/mschapv2/state.go
Normal file
8
internal/outpost/radius/eap/protocol/mschapv2/state.go
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
package mschapv2
|
||||||
|
|
||||||
|
type State struct {
|
||||||
|
Challenge []byte
|
||||||
|
PeerChallenge []byte
|
||||||
|
IsProtocolEnded bool
|
||||||
|
AuthResponse *AuthResponse
|
||||||
|
}
|
31
internal/outpost/radius/eap/protocol/packet.go
Normal file
31
internal/outpost/radius/eap/protocol/packet.go
Normal file
@ -0,0 +1,31 @@
|
|||||||
|
package protocol
|
||||||
|
|
||||||
|
import "layeh.com/radius"
|
||||||
|
|
||||||
|
type Type uint8
|
||||||
|
|
||||||
|
type Code uint8
|
||||||
|
|
||||||
|
const (
|
||||||
|
CodeRequest Code = 1
|
||||||
|
CodeResponse Code = 2
|
||||||
|
CodeSuccess Code = 3
|
||||||
|
CodeFailure Code = 4
|
||||||
|
)
|
||||||
|
|
||||||
|
type Payload interface {
|
||||||
|
Decode(raw []byte) error
|
||||||
|
Encode() ([]byte, error)
|
||||||
|
Handle(ctx Context) Payload
|
||||||
|
Type() Type
|
||||||
|
Offerable() bool
|
||||||
|
String() string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Inner interface {
|
||||||
|
HasInner() Payload
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResponseModifier interface {
|
||||||
|
ModifyRADIUSResponse(r *radius.Packet, q *radius.Packet) error
|
||||||
|
}
|
59
internal/outpost/radius/eap/protocol/peap/extension.go
Normal file
59
internal/outpost/radius/eap/protocol/peap/extension.go
Normal file
@ -0,0 +1,59 @@
|
|||||||
|
package peap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"goauthentik.io/internal/outpost/radius/eap/debug"
|
||||||
|
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||||
|
)
|
||||||
|
|
||||||
|
const TypePEAPExtension protocol.Type = 33
|
||||||
|
|
||||||
|
type ExtensionPayload struct {
|
||||||
|
AVPs []ExtensionAVP
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ep *ExtensionPayload) Decode(raw []byte) error {
|
||||||
|
log.WithField("raw", debug.FormatBytes(raw)).Debugf("PEAP-Extension: decode raw")
|
||||||
|
ep.AVPs = []ExtensionAVP{}
|
||||||
|
offset := 0
|
||||||
|
for {
|
||||||
|
if len(raw[offset:]) < 4 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
len := binary.BigEndian.Uint16(raw[offset+2:offset+2+2]) + ExtensionHeaderSize
|
||||||
|
avp := &ExtensionAVP{}
|
||||||
|
err := avp.Decode(raw[offset : offset+int(len)])
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
ep.AVPs = append(ep.AVPs, *avp)
|
||||||
|
offset = offset + int(len)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ep *ExtensionPayload) Encode() ([]byte, error) {
|
||||||
|
log.Debug("PEAP-Extension: encode")
|
||||||
|
buff := []byte{}
|
||||||
|
for _, avp := range ep.AVPs {
|
||||||
|
buff = append(buff, avp.Encode()...)
|
||||||
|
}
|
||||||
|
return buff, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ep *ExtensionPayload) Handle(protocol.Context) protocol.Payload {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ep *ExtensionPayload) Offerable() bool {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ep *ExtensionPayload) String() string {
|
||||||
|
return "<PEAP Extension Payload>"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (ep *ExtensionPayload) Type() protocol.Type {
|
||||||
|
return TypePEAPExtension
|
||||||
|
}
|
62
internal/outpost/radius/eap/protocol/peap/extension_avp.go
Normal file
62
internal/outpost/radius/eap/protocol/peap/extension_avp.go
Normal file
@ -0,0 +1,62 @@
|
|||||||
|
package peap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AVPType uint16
|
||||||
|
|
||||||
|
const (
|
||||||
|
AVPAckResult AVPType = 3
|
||||||
|
)
|
||||||
|
|
||||||
|
const ExtensionHeaderSize = 4
|
||||||
|
|
||||||
|
type ExtensionAVP struct {
|
||||||
|
Mandatory bool
|
||||||
|
Type AVPType // 14-bit field
|
||||||
|
Length uint16
|
||||||
|
Value []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
ErrorReservedBitSet = errors.New("PEAP-Extension: Reserved bit is not 0")
|
||||||
|
)
|
||||||
|
|
||||||
|
func (eavp *ExtensionAVP) Decode(raw []byte) error {
|
||||||
|
typ := binary.BigEndian.Uint16(raw[:2])
|
||||||
|
if typ>>15 == 1 {
|
||||||
|
eavp.Mandatory = true
|
||||||
|
}
|
||||||
|
if typ>>14&1 != 0 {
|
||||||
|
return ErrorReservedBitSet
|
||||||
|
}
|
||||||
|
eavp.Type = AVPType(typ & 0b0011111111111111)
|
||||||
|
eavp.Length = binary.BigEndian.Uint16(raw[2:4])
|
||||||
|
val := raw[4:]
|
||||||
|
if eavp.Length != uint16(len(val)) {
|
||||||
|
return fmt.Errorf("PEAP-Extension: Invalid length: %d, should be %d", eavp.Length, len(val))
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (eavp ExtensionAVP) Encode() []byte {
|
||||||
|
buff := []byte{
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
}
|
||||||
|
t := uint16(eavp.Type)
|
||||||
|
// Type is a 14-bit number, the highest bit is the mandatory flag
|
||||||
|
if eavp.Mandatory {
|
||||||
|
t = t | 0b1000000000000000
|
||||||
|
}
|
||||||
|
// The next bit is reserved and should always be set to 0
|
||||||
|
t = t & 0b1011111111111111
|
||||||
|
binary.BigEndian.PutUint16(buff[0:], t)
|
||||||
|
binary.BigEndian.PutUint16(buff[2:], uint16(len(eavp.Value)))
|
||||||
|
return append(buff, eavp.Value...)
|
||||||
|
}
|
@ -0,0 +1,36 @@
|
|||||||
|
package peap_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"goauthentik.io/internal/outpost/radius/eap/protocol/peap"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestEncode(t *testing.T) {
|
||||||
|
eavp := peap.ExtensionAVP{
|
||||||
|
Mandatory: true,
|
||||||
|
Type: peap.AVPType(3),
|
||||||
|
}
|
||||||
|
assert.Equal(t, []byte{0x80, 0x3, 0x0, 0x0}, eavp.Encode())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecode(t *testing.T) {
|
||||||
|
eavp := peap.ExtensionAVP{}
|
||||||
|
err := eavp.Decode([]byte{0x80, 0x3, 0x0, 0x0})
|
||||||
|
assert.NoError(t, err)
|
||||||
|
assert.True(t, eavp.Mandatory)
|
||||||
|
assert.Equal(t, peap.AVPType(3), eavp.Type)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecode_Invalid_ReservedBitSet(t *testing.T) {
|
||||||
|
eavp := peap.ExtensionAVP{}
|
||||||
|
err := eavp.Decode([]byte{0xc0, 0x3, 0x0, 0x0})
|
||||||
|
assert.ErrorIs(t, err, peap.ErrorReservedBitSet)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDecode_Invalid_Length(t *testing.T) {
|
||||||
|
eavp := peap.ExtensionAVP{}
|
||||||
|
err := eavp.Decode([]byte{0x80, 0x3, 0x0, 0x0, 0x0})
|
||||||
|
assert.NotNil(t, err)
|
||||||
|
}
|
167
internal/outpost/radius/eap/protocol/peap/payload.go
Normal file
167
internal/outpost/radius/eap/protocol/peap/payload.go
Normal file
@ -0,0 +1,167 @@
|
|||||||
|
package peap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"goauthentik.io/internal/outpost/radius/eap/debug"
|
||||||
|
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||||
|
"goauthentik.io/internal/outpost/radius/eap/protocol/eap"
|
||||||
|
"goauthentik.io/internal/outpost/radius/eap/protocol/identity"
|
||||||
|
"goauthentik.io/internal/outpost/radius/eap/protocol/tls"
|
||||||
|
)
|
||||||
|
|
||||||
|
const TypePEAP protocol.Type = 25
|
||||||
|
|
||||||
|
func Protocol() protocol.Payload {
|
||||||
|
return &tls.Payload{
|
||||||
|
Inner: &Payload{
|
||||||
|
Inner: &eap.Payload{},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Payload struct {
|
||||||
|
Inner protocol.Payload
|
||||||
|
|
||||||
|
eap *eap.Payload
|
||||||
|
st *State
|
||||||
|
settings Settings
|
||||||
|
raw []byte
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payload) Type() protocol.Type {
|
||||||
|
return TypePEAP
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payload) HasInner() protocol.Payload {
|
||||||
|
return p.Inner
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payload) Decode(raw []byte) error {
|
||||||
|
log.WithField("raw", debug.FormatBytes(raw)).Debug("PEAP: Decode")
|
||||||
|
p.raw = raw
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inner EAP packets in PEAP may not include the header, hence we need a custom decoder
|
||||||
|
// https://datatracker.ietf.org/doc/html/draft-kamath-pppext-peapv0-00.txt#section-1.1
|
||||||
|
func (p *Payload) Encode() ([]byte, error) {
|
||||||
|
log.Debug("PEAP: Encoding inner EAP")
|
||||||
|
if p.eap.Payload == nil {
|
||||||
|
return []byte{}, errors.New("PEAP: no payload in response eap packet")
|
||||||
|
}
|
||||||
|
payload, err := p.eap.Payload.Encode()
|
||||||
|
if err != nil {
|
||||||
|
return []byte{}, err
|
||||||
|
}
|
||||||
|
encoded := []byte{
|
||||||
|
byte(p.eap.MsgType),
|
||||||
|
}
|
||||||
|
return append(encoded, payload...), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Inner EAP packets in PEAP may not include the header, hence we need a custom decoder
|
||||||
|
// https://datatracker.ietf.org/doc/html/draft-kamath-pppext-peapv0-00.txt#section-1.1
|
||||||
|
func (p *Payload) eapInnerDecode(ctx protocol.Context) (*eap.Payload, error) {
|
||||||
|
ep := &eap.Payload{
|
||||||
|
Settings: p.GetEAPSettings(),
|
||||||
|
}
|
||||||
|
rootEap := ctx.RootPayload().(*eap.Payload)
|
||||||
|
fixedRaw := []byte{
|
||||||
|
byte(rootEap.Code),
|
||||||
|
rootEap.ID,
|
||||||
|
// 2 byte space for length
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
}
|
||||||
|
fullLength := len(p.raw) + len(fixedRaw)
|
||||||
|
binary.BigEndian.PutUint16(fixedRaw[2:], uint16(fullLength))
|
||||||
|
fixedRaw = append(fixedRaw, p.raw...)
|
||||||
|
// If the raw data has a msgtype set to type 33 (EAP extension), decode differently
|
||||||
|
if len(p.raw) > 5 && p.raw[4] == byte(TypePEAPExtension) {
|
||||||
|
ep.Payload = &ExtensionPayload{}
|
||||||
|
// Pass original raw data to EAP as extension payloads are encoded like normal EAP packets
|
||||||
|
fixedRaw = p.raw
|
||||||
|
}
|
||||||
|
err := ep.Decode(fixedRaw)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return ep, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payload) Handle(ctx protocol.Context) protocol.Payload {
|
||||||
|
defer func() {
|
||||||
|
ctx.SetProtocolState(TypePEAP, p.st)
|
||||||
|
}()
|
||||||
|
p.settings = ctx.ProtocolSettings().(Settings)
|
||||||
|
|
||||||
|
rootEap := ctx.RootPayload().(*eap.Payload)
|
||||||
|
|
||||||
|
if ctx.IsProtocolStart(TypePEAP) {
|
||||||
|
ctx.Log().Debug("PEAP: Protocol start")
|
||||||
|
p.st = &State{
|
||||||
|
SubState: make(map[string]*protocol.State),
|
||||||
|
}
|
||||||
|
return &eap.Payload{
|
||||||
|
Code: protocol.CodeRequest,
|
||||||
|
ID: rootEap.ID + 1,
|
||||||
|
MsgType: identity.TypeIdentity,
|
||||||
|
Payload: &identity.Payload{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.st = ctx.GetProtocolState(TypePEAP).(*State)
|
||||||
|
|
||||||
|
ep, err := p.eapInnerDecode(ctx)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Log().WithError(err).Warning("PEAP: failed to decode inner EAP")
|
||||||
|
return &eap.Payload{
|
||||||
|
Code: protocol.CodeFailure,
|
||||||
|
ID: rootEap.ID + 1,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.eap = ep
|
||||||
|
ctx.Log().Debugf("PEAP: Decoded inner EAP to %s", ep.String())
|
||||||
|
|
||||||
|
res, err := ctx.HandleInnerEAP(ep, p)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Log().WithError(err).Warning("PEAP: failed to handle inner EAP")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// Normal payloads need to be wrapped in PEAP to use the correct encoding (see Encode() above)
|
||||||
|
// Extension payloads handle encoding differently
|
||||||
|
pres := res.(*eap.Payload)
|
||||||
|
if _, ok := pres.Payload.(*ExtensionPayload); ok {
|
||||||
|
// HandleInnerEAP will set the MsgType to the PEAP type, however we need to override that
|
||||||
|
pres.MsgType = TypePEAPExtension
|
||||||
|
ctx.Log().Debug("PEAP: Encoding response as extension")
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
return &Payload{eap: pres}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payload) GetEAPSettings() protocol.Settings {
|
||||||
|
return p.settings.InnerProtocols
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payload) GetEAPState(key string) *protocol.State {
|
||||||
|
return p.st.SubState[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payload) SetEAPState(key string, st *protocol.State) {
|
||||||
|
p.st.SubState[key] = st
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payload) Offerable() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payload) String() string {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"<PEAP Packet Wrapping=%s>",
|
||||||
|
p.eap.String(),
|
||||||
|
)
|
||||||
|
}
|
16
internal/outpost/radius/eap/protocol/peap/settings.go
Normal file
16
internal/outpost/radius/eap/protocol/peap/settings.go
Normal file
@ -0,0 +1,16 @@
|
|||||||
|
package peap
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
|
||||||
|
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Settings struct {
|
||||||
|
Config *tls.Config
|
||||||
|
InnerProtocols protocol.Settings
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Settings) TLSConfig() *tls.Config {
|
||||||
|
return s.Config
|
||||||
|
}
|
7
internal/outpost/radius/eap/protocol/peap/state.go
Normal file
7
internal/outpost/radius/eap/protocol/peap/state.go
Normal file
@ -0,0 +1,7 @@
|
|||||||
|
package peap
|
||||||
|
|
||||||
|
import "goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||||
|
|
||||||
|
type State struct {
|
||||||
|
SubState map[string]*protocol.State
|
||||||
|
}
|
42
internal/outpost/radius/eap/protocol/state.go
Normal file
42
internal/outpost/radius/eap/protocol/state.go
Normal file
@ -0,0 +1,42 @@
|
|||||||
|
package protocol
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"slices"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StateManager interface {
|
||||||
|
GetEAPSettings() Settings
|
||||||
|
GetEAPState(string) *State
|
||||||
|
SetEAPState(string, *State)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ProtocolConstructor func() Payload
|
||||||
|
|
||||||
|
type Settings struct {
|
||||||
|
Protocols []ProtocolConstructor
|
||||||
|
ProtocolPriority []Type
|
||||||
|
ProtocolSettings map[Type]interface{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type State struct {
|
||||||
|
Protocols []ProtocolConstructor
|
||||||
|
ProtocolIndex int
|
||||||
|
ProtocolPriority []Type
|
||||||
|
TypeState map[Type]any
|
||||||
|
}
|
||||||
|
|
||||||
|
func (st *State) GetNextProtocol() (Type, error) {
|
||||||
|
if st.ProtocolIndex >= len(st.ProtocolPriority) {
|
||||||
|
return Type(0), errors.New("no more protocols to offer")
|
||||||
|
}
|
||||||
|
return st.ProtocolPriority[st.ProtocolIndex], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func BlankState(settings Settings) *State {
|
||||||
|
return &State{
|
||||||
|
Protocols: slices.Clone(settings.Protocols),
|
||||||
|
ProtocolPriority: slices.Clone(settings.ProtocolPriority),
|
||||||
|
TypeState: map[Type]any{},
|
||||||
|
}
|
||||||
|
}
|
111
internal/outpost/radius/eap/protocol/tls/buff_conn.go
Normal file
111
internal/outpost/radius/eap/protocol/tls/buff_conn.go
Normal file
@ -0,0 +1,111 @@
|
|||||||
|
package tls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"net"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/avast/retry-go/v4"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
)
|
||||||
|
|
||||||
|
type BuffConn struct {
|
||||||
|
reader *bytes.Buffer
|
||||||
|
writer *bytes.Buffer
|
||||||
|
|
||||||
|
ctx context.Context
|
||||||
|
|
||||||
|
expectedWriterByteCount int
|
||||||
|
writtenByteCount int
|
||||||
|
|
||||||
|
retryOptions []retry.Option
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBuffConn(initialData []byte, ctx context.Context) *BuffConn {
|
||||||
|
c := &BuffConn{
|
||||||
|
reader: bytes.NewBuffer(initialData),
|
||||||
|
writer: bytes.NewBuffer([]byte{}),
|
||||||
|
ctx: ctx,
|
||||||
|
retryOptions: []retry.Option{
|
||||||
|
retry.Context(ctx),
|
||||||
|
retry.Delay(10 * time.Microsecond),
|
||||||
|
retry.DelayType(retry.BackOffDelay),
|
||||||
|
retry.MaxDelay(100 * time.Millisecond),
|
||||||
|
retry.Attempts(0),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
return c
|
||||||
|
}
|
||||||
|
|
||||||
|
var errStall = errors.New("Stall")
|
||||||
|
|
||||||
|
func (conn BuffConn) OutboundData() []byte {
|
||||||
|
d, _ := retry.DoWithData(
|
||||||
|
func() ([]byte, error) {
|
||||||
|
b := conn.writer.Bytes()
|
||||||
|
if len(b) < 1 {
|
||||||
|
return nil, errStall
|
||||||
|
}
|
||||||
|
return b, nil
|
||||||
|
},
|
||||||
|
conn.retryOptions...,
|
||||||
|
)
|
||||||
|
return d
|
||||||
|
}
|
||||||
|
|
||||||
|
func (conn *BuffConn) UpdateData(data []byte) {
|
||||||
|
conn.reader.Write(data)
|
||||||
|
conn.writtenByteCount += len(data)
|
||||||
|
log.Debugf("TLS(buffcon): Appending new data %d (total %d, expecting %d)", len(data), conn.writtenByteCount, conn.expectedWriterByteCount)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (conn BuffConn) NeedsMoreData() bool {
|
||||||
|
if conn.expectedWriterByteCount > 0 {
|
||||||
|
return conn.reader.Len() < int(conn.expectedWriterByteCount)
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (conn *BuffConn) Read(p []byte) (int, error) {
|
||||||
|
d, err := retry.DoWithData(
|
||||||
|
func() (int, error) {
|
||||||
|
if conn.reader.Len() == 0 {
|
||||||
|
log.Debugf("TLS(buffcon): Attempted read %d from empty buffer, stalling...", len(p))
|
||||||
|
return 0, errStall
|
||||||
|
}
|
||||||
|
if conn.expectedWriterByteCount > 0 {
|
||||||
|
// If we're waiting for more data, we need to stall
|
||||||
|
if conn.writtenByteCount < int(conn.expectedWriterByteCount) {
|
||||||
|
log.Debugf("TLS(buffcon): Attempted read %d while waiting for bytes %d, stalling...", len(p), conn.expectedWriterByteCount-conn.reader.Len())
|
||||||
|
return 0, errStall
|
||||||
|
}
|
||||||
|
// If we have all the data, reset how much we're expecting to still get
|
||||||
|
if conn.writtenByteCount == int(conn.expectedWriterByteCount) {
|
||||||
|
conn.expectedWriterByteCount = 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if conn.reader.Len() == 0 {
|
||||||
|
conn.writtenByteCount = 0
|
||||||
|
}
|
||||||
|
n, err := conn.reader.Read(p)
|
||||||
|
log.Debugf("TLS(buffcon): Read: %d into %d (total %d)", n, len(p), conn.reader.Len())
|
||||||
|
return n, err
|
||||||
|
},
|
||||||
|
conn.retryOptions...,
|
||||||
|
)
|
||||||
|
return d, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (conn BuffConn) Write(p []byte) (int, error) {
|
||||||
|
log.Debugf("TLS(buffcon): Write: %d", len(p))
|
||||||
|
return conn.writer.Write(p)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (conn BuffConn) Close() error { return nil }
|
||||||
|
func (conn BuffConn) LocalAddr() net.Addr { return nil }
|
||||||
|
func (conn BuffConn) RemoteAddr() net.Addr { return nil }
|
||||||
|
func (conn BuffConn) SetDeadline(t time.Time) error { return nil }
|
||||||
|
func (conn BuffConn) SetReadDeadline(t time.Time) error { return nil }
|
||||||
|
func (conn BuffConn) SetWriteDeadline(t time.Time) error { return nil }
|
10
internal/outpost/radius/eap/protocol/tls/flags.go
Normal file
10
internal/outpost/radius/eap/protocol/tls/flags.go
Normal file
@ -0,0 +1,10 @@
|
|||||||
|
package tls
|
||||||
|
|
||||||
|
type Flag byte
|
||||||
|
|
||||||
|
const (
|
||||||
|
FlagLengthIncluded Flag = 1 << 7
|
||||||
|
FlagMoreFragments Flag = 1 << 6
|
||||||
|
FlagTLSStart Flag = 1 << 5
|
||||||
|
FlagNone Flag = 0
|
||||||
|
)
|
39
internal/outpost/radius/eap/protocol/tls/inner.go
Normal file
39
internal/outpost/radius/eap/protocol/tls/inner.go
Normal file
@ -0,0 +1,39 @@
|
|||||||
|
package tls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (p *Payload) innerHandler(ctx protocol.Context) {
|
||||||
|
d := make([]byte, 1024)
|
||||||
|
if !ctx.IsProtocolStart(p.Inner.Type()) {
|
||||||
|
ctx.Log().Debug("TLS: Reading from TLS for inner protocol")
|
||||||
|
n, err := p.st.TLS.Read(d)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Log().WithError(err).Warning("TLS: Failed to read from TLS connection")
|
||||||
|
ctx.EndInnerProtocol(protocol.StatusError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
// Truncate data to the size we read
|
||||||
|
d = d[:n]
|
||||||
|
}
|
||||||
|
err := p.Inner.Decode(d)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Log().WithError(err).Warning("TLS: failed to decode inner protocol")
|
||||||
|
ctx.EndInnerProtocol(protocol.StatusError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
pl := p.Inner.Handle(ctx.Inner(p.Inner, p.Inner.Type()))
|
||||||
|
enc, err := pl.Encode()
|
||||||
|
if err != nil {
|
||||||
|
ctx.Log().WithError(err).Warning("TLS: failed to encode inner protocol")
|
||||||
|
ctx.EndInnerProtocol(protocol.StatusError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, err = p.st.TLS.Write(enc)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Log().WithError(err).Warning("TLS: failed to write to TLS")
|
||||||
|
ctx.EndInnerProtocol(protocol.StatusError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
279
internal/outpost/radius/eap/protocol/tls/payload.go
Normal file
279
internal/outpost/radius/eap/protocol/tls/payload.go
Normal file
@ -0,0 +1,279 @@
|
|||||||
|
package tls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/binary"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"slices"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/avast/retry-go/v4"
|
||||||
|
log "github.com/sirupsen/logrus"
|
||||||
|
"goauthentik.io/internal/outpost/radius/eap/debug"
|
||||||
|
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||||
|
"layeh.com/radius"
|
||||||
|
"layeh.com/radius/vendors/microsoft"
|
||||||
|
)
|
||||||
|
|
||||||
|
const maxChunkSize = 1000
|
||||||
|
const staleConnectionTimeout = 10
|
||||||
|
|
||||||
|
const TypeTLS protocol.Type = 13
|
||||||
|
|
||||||
|
func Protocol() protocol.Payload {
|
||||||
|
return &Payload{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Payload struct {
|
||||||
|
Flags Flag
|
||||||
|
Length uint32
|
||||||
|
Data []byte
|
||||||
|
|
||||||
|
st *State
|
||||||
|
Inner protocol.Payload
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payload) Type() protocol.Type {
|
||||||
|
return TypeTLS
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payload) HasInner() protocol.Payload {
|
||||||
|
return p.Inner
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payload) Offerable() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payload) Decode(raw []byte) error {
|
||||||
|
p.Flags = Flag(raw[0])
|
||||||
|
raw = raw[1:]
|
||||||
|
if p.Flags&FlagLengthIncluded != 0 {
|
||||||
|
if len(raw) < 4 {
|
||||||
|
return errors.New("invalid size")
|
||||||
|
}
|
||||||
|
p.Length = binary.BigEndian.Uint32(raw)
|
||||||
|
p.Data = raw[4:]
|
||||||
|
} else {
|
||||||
|
p.Data = raw[0:]
|
||||||
|
}
|
||||||
|
log.WithField("raw", debug.FormatBytes(p.Data)).WithField("size", len(p.Data)).WithField("flags", p.Flags).Trace("TLS: decode raw")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payload) Encode() ([]byte, error) {
|
||||||
|
l := 1
|
||||||
|
if p.Flags&FlagLengthIncluded != 0 {
|
||||||
|
l += 4
|
||||||
|
}
|
||||||
|
buff := make([]byte, len(p.Data)+l)
|
||||||
|
buff[0] = byte(p.Flags)
|
||||||
|
if p.Flags&FlagLengthIncluded != 0 {
|
||||||
|
buff[1] = byte(p.Length >> 24)
|
||||||
|
buff[2] = byte(p.Length >> 16)
|
||||||
|
buff[3] = byte(p.Length >> 8)
|
||||||
|
buff[4] = byte(p.Length)
|
||||||
|
}
|
||||||
|
if len(p.Data) > 0 {
|
||||||
|
copy(buff[5:], p.Data)
|
||||||
|
}
|
||||||
|
return buff, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payload) Handle(ctx protocol.Context) protocol.Payload {
|
||||||
|
defer func() {
|
||||||
|
ctx.SetProtocolState(TypeTLS, p.st)
|
||||||
|
}()
|
||||||
|
if ctx.IsProtocolStart(TypeTLS) {
|
||||||
|
p.st = NewState(ctx).(*State)
|
||||||
|
return &Payload{
|
||||||
|
Flags: FlagTLSStart,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
p.st = ctx.GetProtocolState(TypeTLS).(*State)
|
||||||
|
|
||||||
|
if p.st.TLS == nil {
|
||||||
|
p.tlsInit(ctx)
|
||||||
|
} else if len(p.Data) > 0 {
|
||||||
|
ctx.Log().Debug("TLS: Updating buffer with new TLS data from packet")
|
||||||
|
if p.Flags&FlagLengthIncluded != 0 && p.st.Conn.expectedWriterByteCount == 0 {
|
||||||
|
ctx.Log().Debugf("TLS: Expecting %d total bytes, will buffer", p.Length)
|
||||||
|
p.st.Conn.expectedWriterByteCount = int(p.Length)
|
||||||
|
} else if p.Flags&FlagLengthIncluded != 0 {
|
||||||
|
ctx.Log().Debug("TLS: No length included, not buffering")
|
||||||
|
p.st.Conn.expectedWriterByteCount = 0
|
||||||
|
}
|
||||||
|
p.st.Conn.UpdateData(p.Data)
|
||||||
|
if !p.st.Conn.NeedsMoreData() && !p.st.HandshakeDone {
|
||||||
|
// Wait for outbound data to be available
|
||||||
|
p.st.Conn.OutboundData()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// If we need more data, send the client the go-ahead
|
||||||
|
if p.st.Conn.NeedsMoreData() {
|
||||||
|
return &Payload{
|
||||||
|
Flags: FlagNone,
|
||||||
|
Length: 0,
|
||||||
|
Data: []byte{},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if p.st.HasMore() {
|
||||||
|
return p.sendNextChunk()
|
||||||
|
}
|
||||||
|
if p.st.Conn.writer.Len() == 0 && p.st.HandshakeDone {
|
||||||
|
if p.Inner != nil {
|
||||||
|
ctx.Log().Debug("TLS: Handshake is done, delegating to inner protocol")
|
||||||
|
p.innerHandler(ctx)
|
||||||
|
return p.startChunkedTransfer(p.st.Conn.OutboundData())
|
||||||
|
}
|
||||||
|
defer p.st.ContextCancel()
|
||||||
|
// If we don't have a final status from the handshake finished function, stall for time
|
||||||
|
pst, _ := retry.DoWithData(
|
||||||
|
func() (protocol.Status, error) {
|
||||||
|
if p.st.FinalStatus == protocol.StatusUnknown {
|
||||||
|
return p.st.FinalStatus, errStall
|
||||||
|
}
|
||||||
|
return p.st.FinalStatus, nil
|
||||||
|
},
|
||||||
|
retry.Context(p.st.Context),
|
||||||
|
retry.Delay(10*time.Microsecond),
|
||||||
|
retry.DelayType(retry.BackOffDelay),
|
||||||
|
retry.MaxDelay(100*time.Millisecond),
|
||||||
|
retry.Attempts(0),
|
||||||
|
)
|
||||||
|
ctx.EndInnerProtocol(pst)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return p.startChunkedTransfer(p.st.Conn.OutboundData())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payload) ModifyRADIUSResponse(r *radius.Packet, q *radius.Packet) error {
|
||||||
|
if r.Code != radius.CodeAccessAccept {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if p.st == nil || !p.st.HandshakeDone {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
log.Debug("TLS: Adding MPPE Keys")
|
||||||
|
// TLS overrides other protocols' MPPE keys
|
||||||
|
if len(microsoft.MSMPPERecvKey_Get(r, q)) > 0 {
|
||||||
|
microsoft.MSMPPERecvKey_Del(r)
|
||||||
|
}
|
||||||
|
if len(microsoft.MSMPPESendKey_Get(r, q)) > 0 {
|
||||||
|
microsoft.MSMPPESendKey_Del(r)
|
||||||
|
}
|
||||||
|
microsoft.MSMPPERecvKey_Set(r, p.st.MPPEKey[:32])
|
||||||
|
microsoft.MSMPPESendKey_Set(r, p.st.MPPEKey[64:64+32])
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payload) tlsInit(ctx protocol.Context) {
|
||||||
|
ctx.Log().Debug("TLS: no TLS connection in state yet, starting connection")
|
||||||
|
p.st.Context, p.st.ContextCancel = context.WithTimeout(context.Background(), staleConnectionTimeout*time.Second)
|
||||||
|
p.st.Conn = NewBuffConn(p.Data, p.st.Context)
|
||||||
|
cfg := ctx.ProtocolSettings().(TLSConfig).TLSConfig().Clone()
|
||||||
|
|
||||||
|
if klp, ok := os.LookupEnv("SSLKEYLOGFILE"); ok {
|
||||||
|
kl, err := os.OpenFile(klp, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0600)
|
||||||
|
if err != nil {
|
||||||
|
panic(err)
|
||||||
|
}
|
||||||
|
cfg.KeyLogWriter = kl
|
||||||
|
}
|
||||||
|
|
||||||
|
cfg.GetConfigForClient = func(chi *tls.ClientHelloInfo) (*tls.Config, error) {
|
||||||
|
ctx.Log().Debugf("TLS: ClientHello: %+v\n", chi)
|
||||||
|
p.st.ClientHello = chi
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
p.st.TLS = tls.Server(p.st.Conn, cfg)
|
||||||
|
p.st.TLS.SetDeadline(time.Now().Add(staleConnectionTimeout * time.Second))
|
||||||
|
go func() {
|
||||||
|
err := p.st.TLS.HandshakeContext(p.st.Context)
|
||||||
|
if err != nil {
|
||||||
|
ctx.Log().WithError(err).Debug("TLS: Handshake error")
|
||||||
|
p.st.FinalStatus = protocol.StatusError
|
||||||
|
ctx.EndInnerProtocol(protocol.StatusError)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ctx.Log().Debug("TLS: handshake done")
|
||||||
|
p.tlsHandshakeFinished(ctx)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payload) tlsHandshakeFinished(ctx protocol.Context) {
|
||||||
|
cs := p.st.TLS.ConnectionState()
|
||||||
|
label := "client EAP encryption"
|
||||||
|
var context []byte
|
||||||
|
switch cs.Version {
|
||||||
|
case tls.VersionTLS10:
|
||||||
|
ctx.Log().Debugf("TLS: Version %d (1.0)", cs.Version)
|
||||||
|
case tls.VersionTLS11:
|
||||||
|
ctx.Log().Debugf("TLS: Version %d (1.1)", cs.Version)
|
||||||
|
case tls.VersionTLS12:
|
||||||
|
ctx.Log().Debugf("TLS: Version %d (1.2)", cs.Version)
|
||||||
|
case tls.VersionTLS13:
|
||||||
|
ctx.Log().Debugf("TLS: Version %d (1.3)", cs.Version)
|
||||||
|
label = "EXPORTER_EAP_TLS_Key_Material"
|
||||||
|
context = []byte{byte(TypeTLS)}
|
||||||
|
}
|
||||||
|
ksm, err := cs.ExportKeyingMaterial(label, context, 64+64)
|
||||||
|
ctx.Log().Debugf("TLS: ksm % x %v", ksm, err)
|
||||||
|
p.st.MPPEKey = ksm
|
||||||
|
p.st.HandshakeDone = true
|
||||||
|
if p.Inner == nil {
|
||||||
|
p.st.FinalStatus = ctx.ProtocolSettings().(Settings).HandshakeSuccessful(ctx, cs.PeerCertificates)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payload) startChunkedTransfer(data []byte) *Payload {
|
||||||
|
if len(data) > maxChunkSize {
|
||||||
|
log.WithField("length", len(data)).Debug("TLS: Data needs to be chunked")
|
||||||
|
p.st.RemainingChunks = append(p.st.RemainingChunks, slices.Collect(slices.Chunk(data, maxChunkSize))...)
|
||||||
|
p.st.TotalPayloadSize = len(data)
|
||||||
|
return p.sendNextChunk()
|
||||||
|
}
|
||||||
|
log.WithField("length", len(data)).Debug("TLS: Sending data un-chunked")
|
||||||
|
p.st.Conn.writer.Reset()
|
||||||
|
return &Payload{
|
||||||
|
Flags: FlagLengthIncluded,
|
||||||
|
Length: uint32(len(data)),
|
||||||
|
Data: data,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payload) sendNextChunk() *Payload {
|
||||||
|
nextChunk := p.st.RemainingChunks[0]
|
||||||
|
log.WithField("raw", debug.FormatBytes(nextChunk)).Debug("TLS: Sending next chunk")
|
||||||
|
p.st.RemainingChunks = p.st.RemainingChunks[1:]
|
||||||
|
flags := FlagLengthIncluded
|
||||||
|
if p.st.HasMore() {
|
||||||
|
log.WithField("chunks", len(p.st.RemainingChunks)).Debug("TLS: More chunks left")
|
||||||
|
flags += FlagMoreFragments
|
||||||
|
} else {
|
||||||
|
// Last chunk, reset the connection buffers and pending payload size
|
||||||
|
defer func() {
|
||||||
|
log.Debug("TLS: Sent last chunk")
|
||||||
|
p.st.Conn.writer.Reset()
|
||||||
|
p.st.TotalPayloadSize = 0
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
log.WithField("length", p.st.TotalPayloadSize).Debug("TLS: Total payload size")
|
||||||
|
return &Payload{
|
||||||
|
Flags: flags,
|
||||||
|
Length: uint32(p.st.TotalPayloadSize),
|
||||||
|
Data: nextChunk,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (p *Payload) String() string {
|
||||||
|
return fmt.Sprintf(
|
||||||
|
"<TLS Packet HandshakeDone=%t, FinalStatus=%d, ClientHello=%v>",
|
||||||
|
p.st.HandshakeDone,
|
||||||
|
p.st.FinalStatus,
|
||||||
|
p.st.ClientHello,
|
||||||
|
)
|
||||||
|
}
|
21
internal/outpost/radius/eap/protocol/tls/settings.go
Normal file
21
internal/outpost/radius/eap/protocol/tls/settings.go
Normal file
@ -0,0 +1,21 @@
|
|||||||
|
package tls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
|
|
||||||
|
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TLSConfig interface {
|
||||||
|
TLSConfig() *tls.Config
|
||||||
|
}
|
||||||
|
|
||||||
|
type Settings struct {
|
||||||
|
Config *tls.Config
|
||||||
|
HandshakeSuccessful func(ctx protocol.Context, certs []*x509.Certificate) protocol.Status
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s Settings) TLSConfig() *tls.Config {
|
||||||
|
return s.Config
|
||||||
|
}
|
32
internal/outpost/radius/eap/protocol/tls/state.go
Normal file
32
internal/outpost/radius/eap/protocol/tls/state.go
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
package tls
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
|
||||||
|
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||||
|
)
|
||||||
|
|
||||||
|
type State struct {
|
||||||
|
RemainingChunks [][]byte
|
||||||
|
HandshakeDone bool
|
||||||
|
FinalStatus protocol.Status
|
||||||
|
ClientHello *tls.ClientHelloInfo
|
||||||
|
MPPEKey []byte
|
||||||
|
TotalPayloadSize int
|
||||||
|
TLS *tls.Conn
|
||||||
|
Conn *BuffConn
|
||||||
|
Context context.Context
|
||||||
|
ContextCancel context.CancelFunc
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewState(c protocol.Context) interface{} {
|
||||||
|
c.Log().Debug("TLS: new state")
|
||||||
|
return &State{
|
||||||
|
RemainingChunks: make([][]byte, 0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s State) HasMore() bool {
|
||||||
|
return len(s.RemainingChunks) > 0
|
||||||
|
}
|
@ -1,17 +1,44 @@
|
|||||||
package radius
|
package radius
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
ttls "crypto/tls"
|
||||||
|
"crypto/x509"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"encoding/pem"
|
||||||
|
"net/url"
|
||||||
|
|
||||||
"github.com/prometheus/client_golang/prometheus"
|
"github.com/prometheus/client_golang/prometheus"
|
||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
|
"goauthentik.io/api/v3"
|
||||||
"goauthentik.io/internal/outpost/flow"
|
"goauthentik.io/internal/outpost/flow"
|
||||||
|
"goauthentik.io/internal/outpost/radius/eap"
|
||||||
|
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||||
|
"goauthentik.io/internal/outpost/radius/eap/protocol/gtc"
|
||||||
|
"goauthentik.io/internal/outpost/radius/eap/protocol/identity"
|
||||||
|
"goauthentik.io/internal/outpost/radius/eap/protocol/legacy_nak"
|
||||||
|
"goauthentik.io/internal/outpost/radius/eap/protocol/mschapv2"
|
||||||
|
"goauthentik.io/internal/outpost/radius/eap/protocol/peap"
|
||||||
|
"goauthentik.io/internal/outpost/radius/eap/protocol/tls"
|
||||||
"goauthentik.io/internal/outpost/radius/metrics"
|
"goauthentik.io/internal/outpost/radius/metrics"
|
||||||
|
"goauthentik.io/internal/utils"
|
||||||
"layeh.com/radius"
|
"layeh.com/radius"
|
||||||
"layeh.com/radius/rfc2865"
|
"layeh.com/radius/rfc2865"
|
||||||
|
"layeh.com/radius/rfc2869"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (rs *RadiusServer) Handle_AccessRequest(w radius.ResponseWriter, r *RadiusRequest) {
|
func (rs *RadiusServer) Handle_AccessRequest(w radius.ResponseWriter, r *RadiusRequest) {
|
||||||
|
eap := rfc2869.EAPMessage_Get(r.Packet)
|
||||||
|
if len(eap) > 0 {
|
||||||
|
rs.log.Trace("EAP request")
|
||||||
|
rs.Handle_AccessRequest_EAP(w, r)
|
||||||
|
} else {
|
||||||
|
rs.log.Trace("PAP request")
|
||||||
|
rs.Handle_AccessRequest_PAP(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (rs *RadiusServer) Handle_AccessRequest_PAP(w radius.ResponseWriter, r *RadiusRequest) {
|
||||||
username := rfc2865.UserName_GetString(r.Packet)
|
username := rfc2865.UserName_GetString(r.Packet)
|
||||||
|
|
||||||
fe := flow.NewFlowExecutor(r.Context(), r.pi.flowSlug, r.pi.s.ac.Client.GetConfig(), log.Fields{
|
fe := flow.NewFlowExecutor(r.Context(), r.pi.flowSlug, r.pi.s.ac.Client.GetConfig(), log.Fields{
|
||||||
@ -87,3 +114,164 @@ func (rs *RadiusServer) Handle_AccessRequest(w radius.ResponseWriter, r *RadiusR
|
|||||||
res.Add(attr.Type, attr.Attribute)
|
res.Add(attr.Type, attr.Attribute)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (rs *RadiusServer) Handle_AccessRequest_EAP(w radius.ResponseWriter, r *RadiusRequest) {
|
||||||
|
er := rfc2869.EAPMessage_Get(r.Packet)
|
||||||
|
ep, err := eap.Decode(r.pi, er)
|
||||||
|
if err != nil {
|
||||||
|
rs.log.WithError(err).Warning("failed to parse EAP packet")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ep.HandleRadiusPacket(w, r.Request)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pi *ProviderInstance) GetEAPState(key string) *protocol.State {
|
||||||
|
return pi.eapState[key]
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pi *ProviderInstance) SetEAPState(key string, state *protocol.State) {
|
||||||
|
pi.eapState[key] = state
|
||||||
|
}
|
||||||
|
|
||||||
|
func (pi *ProviderInstance) GetEAPSettings() protocol.Settings {
|
||||||
|
protocols := []protocol.ProtocolConstructor{
|
||||||
|
identity.Protocol,
|
||||||
|
legacy_nak.Protocol,
|
||||||
|
}
|
||||||
|
|
||||||
|
certId := pi.certId
|
||||||
|
if certId == "" {
|
||||||
|
return protocol.Settings{
|
||||||
|
Protocols: protocols,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cert := pi.s.cryptoStore.Get(certId)
|
||||||
|
if cert == nil {
|
||||||
|
return protocol.Settings{
|
||||||
|
Protocols: protocols,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return protocol.Settings{
|
||||||
|
Protocols: append(protocols, tls.Protocol, peap.Protocol),
|
||||||
|
ProtocolPriority: []protocol.Type{tls.TypeTLS, peap.TypePEAP},
|
||||||
|
ProtocolSettings: map[protocol.Type]interface{}{
|
||||||
|
tls.TypeTLS: tls.Settings{
|
||||||
|
Config: &ttls.Config{
|
||||||
|
Certificates: []ttls.Certificate{*cert},
|
||||||
|
ClientAuth: ttls.RequireAnyClientCert,
|
||||||
|
},
|
||||||
|
HandshakeSuccessful: func(ctx protocol.Context, certs []*x509.Certificate) protocol.Status {
|
||||||
|
ctx.Log().Debug("Starting authn flow")
|
||||||
|
pem := pem.EncodeToMemory(&pem.Block{
|
||||||
|
Type: "CERTIFICATE",
|
||||||
|
Bytes: certs[0].Raw,
|
||||||
|
})
|
||||||
|
|
||||||
|
fe := flow.NewFlowExecutor(context.Background(), pi.flowSlug, pi.s.ac.Client.GetConfig(), log.Fields{
|
||||||
|
"client": utils.GetIP(ctx.Packet().RemoteAddr),
|
||||||
|
})
|
||||||
|
fe.DelegateClientIP(utils.GetIP(ctx.Packet().RemoteAddr))
|
||||||
|
fe.Params.Add("goauthentik.io/outpost/radius", "true")
|
||||||
|
fe.AddHeader("X-Authentik-Outpost-Certificate", url.QueryEscape(string(pem)))
|
||||||
|
|
||||||
|
passed, err := fe.Execute()
|
||||||
|
if err != nil {
|
||||||
|
ctx.Log().WithError(err).Warning("failed to execute flow")
|
||||||
|
return protocol.StatusError
|
||||||
|
}
|
||||||
|
ctx.Log().WithField("passed", passed).Debug("Finished flow")
|
||||||
|
if passed {
|
||||||
|
return protocol.StatusSuccess
|
||||||
|
} else {
|
||||||
|
return protocol.StatusError
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
peap.TypePEAP: peap.Settings{
|
||||||
|
Config: &ttls.Config{
|
||||||
|
Certificates: []ttls.Certificate{*cert},
|
||||||
|
},
|
||||||
|
InnerProtocols: protocol.Settings{
|
||||||
|
Protocols: append(protocols, gtc.Protocol, mschapv2.Protocol),
|
||||||
|
ProtocolPriority: []protocol.Type{gtc.TypeGTC, mschapv2.TypeMSCHAPv2},
|
||||||
|
ProtocolSettings: map[protocol.Type]interface{}{
|
||||||
|
mschapv2.TypeMSCHAPv2: mschapv2.Settings{
|
||||||
|
AuthenticateRequest: mschapv2.DebugStaticCredentials(
|
||||||
|
[]byte("foo"), []byte("bar"),
|
||||||
|
),
|
||||||
|
},
|
||||||
|
gtc.TypeGTC: gtc.Settings{
|
||||||
|
ChallengeHandler: func(ctx protocol.Context) (gtc.GetChallenge, gtc.ValidateResponse) {
|
||||||
|
fe := flow.NewFlowExecutor(context.Background(), pi.flowSlug, pi.s.ac.Client.GetConfig(), log.Fields{
|
||||||
|
"client": utils.GetIP(ctx.Packet().RemoteAddr),
|
||||||
|
})
|
||||||
|
fe.DelegateClientIP(utils.GetIP(ctx.Packet().RemoteAddr))
|
||||||
|
fe.Params.Add("goauthentik.io/outpost/radius", "true")
|
||||||
|
var ch []byte = nil
|
||||||
|
var ans []byte = nil
|
||||||
|
fe.InteractiveSolver = func(ct *api.ChallengeTypes, afesr api.ApiFlowsExecutorSolveRequest) (api.FlowChallengeResponseRequest, error) {
|
||||||
|
comp := ct.GetActualInstance().(flow.ChallengeCommon).GetComponent()
|
||||||
|
ch = []byte(comp)
|
||||||
|
for {
|
||||||
|
if ans == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
break
|
||||||
|
}
|
||||||
|
switch comp {
|
||||||
|
case string(flow.StageIdentification):
|
||||||
|
r := api.NewIdentificationChallengeResponseRequest(string(ans))
|
||||||
|
return api.IdentificationChallengeResponseRequestAsFlowChallengeResponseRequest(r), nil
|
||||||
|
case string(flow.StagePassword):
|
||||||
|
r := api.NewPasswordChallengeResponseRequest(string(ans))
|
||||||
|
return api.PasswordChallengeResponseRequestAsFlowChallengeResponseRequest(r), nil
|
||||||
|
}
|
||||||
|
panic(comp)
|
||||||
|
}
|
||||||
|
passed := false
|
||||||
|
done := false
|
||||||
|
go func() {
|
||||||
|
var err error
|
||||||
|
passed, err = fe.Execute()
|
||||||
|
done = true
|
||||||
|
if err != nil {
|
||||||
|
ctx.Log().WithError(err).Warning("failed to execute flow")
|
||||||
|
// return protocol.StatusError
|
||||||
|
}
|
||||||
|
// ctx.Log().WithField("passed", passed).Debug("Finished flow")
|
||||||
|
// if passed {
|
||||||
|
// return protocol.StatusSuccess
|
||||||
|
// } else {
|
||||||
|
// return protocol.StatusError
|
||||||
|
// }
|
||||||
|
}()
|
||||||
|
return func() []byte {
|
||||||
|
if done {
|
||||||
|
status := protocol.StatusError
|
||||||
|
if passed {
|
||||||
|
status = protocol.StatusSuccess
|
||||||
|
}
|
||||||
|
ctx.EndInnerProtocol(status)
|
||||||
|
}
|
||||||
|
for {
|
||||||
|
if ch == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
ch = nil
|
||||||
|
}()
|
||||||
|
return ch
|
||||||
|
}
|
||||||
|
}, func(answer []byte) {
|
||||||
|
ans = answer
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@ -3,6 +3,7 @@ package radius
|
|||||||
import (
|
import (
|
||||||
"crypto/sha512"
|
"crypto/sha512"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
|
"net"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/getsentry/sentry-go"
|
"github.com/getsentry/sentry-go"
|
||||||
@ -35,12 +36,32 @@ func (r *RadiusRequest) ID() string {
|
|||||||
return r.id
|
return r.id
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type LogWriter struct {
|
||||||
|
w radius.ResponseWriter
|
||||||
|
l *log.Entry
|
||||||
|
}
|
||||||
|
|
||||||
|
func (lw LogWriter) Write(packet *radius.Packet) error {
|
||||||
|
lw.l.WithField("code", packet.Code.String()).Info("Radius Response")
|
||||||
|
return lw.w.Write(packet)
|
||||||
|
}
|
||||||
|
|
||||||
func (rs *RadiusServer) ServeRADIUS(w radius.ResponseWriter, r *radius.Request) {
|
func (rs *RadiusServer) ServeRADIUS(w radius.ResponseWriter, r *radius.Request) {
|
||||||
span := sentry.StartSpan(r.Context(), "authentik.providers.radius.connect",
|
span := sentry.StartSpan(r.Context(), "authentik.providers.radius.connect",
|
||||||
sentry.WithTransactionName("authentik.providers.radius.connect"))
|
sentry.WithTransactionName("authentik.providers.radius.connect"))
|
||||||
rid := uuid.New().String()
|
rid := uuid.New().String()
|
||||||
span.SetTag("request_uid", rid)
|
span.SetTag("request_uid", rid)
|
||||||
rl := rs.log.WithField("code", r.Code.String()).WithField("request", rid)
|
host, _, err := net.SplitHostPort(r.RemoteAddr.String())
|
||||||
|
if err != nil {
|
||||||
|
rs.log.WithError(err).Warning("Failed to get remote IP")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
rl := rs.log.WithFields(log.Fields{
|
||||||
|
"code": r.Code.String(),
|
||||||
|
"request": rid,
|
||||||
|
"ip": host,
|
||||||
|
"id": r.Identifier,
|
||||||
|
})
|
||||||
selectedApp := ""
|
selectedApp := ""
|
||||||
defer func() {
|
defer func() {
|
||||||
span.Finish()
|
span.Finish()
|
||||||
@ -58,6 +79,7 @@ func (rs *RadiusServer) ServeRADIUS(w radius.ResponseWriter, r *radius.Request)
|
|||||||
}
|
}
|
||||||
|
|
||||||
rl.Info("Radius Request")
|
rl.Info("Radius Request")
|
||||||
|
ww := LogWriter{w, rl}
|
||||||
|
|
||||||
// Lookup provider by shared secret
|
// Lookup provider by shared secret
|
||||||
var pi *ProviderInstance
|
var pi *ProviderInstance
|
||||||
@ -72,12 +94,12 @@ func (rs *RadiusServer) ServeRADIUS(w radius.ResponseWriter, r *radius.Request)
|
|||||||
hs := sha512.Sum512([]byte(r.Secret))
|
hs := sha512.Sum512([]byte(r.Secret))
|
||||||
bs := hex.EncodeToString(hs[:])
|
bs := hex.EncodeToString(hs[:])
|
||||||
nr.Log().WithField("hashed_secret", bs).Warning("No provider found")
|
nr.Log().WithField("hashed_secret", bs).Warning("No provider found")
|
||||||
_ = w.Write(r.Response(radius.CodeAccessReject))
|
_ = ww.Write(r.Response(radius.CodeAccessReject))
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
nr.pi = pi
|
nr.pi = pi
|
||||||
|
|
||||||
if nr.Code == radius.CodeAccessRequest {
|
if nr.Code == radius.CodeAccessRequest {
|
||||||
rs.Handle_AccessRequest(w, nr)
|
rs.Handle_AccessRequest(ww, nr)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -9,6 +9,7 @@ import (
|
|||||||
log "github.com/sirupsen/logrus"
|
log "github.com/sirupsen/logrus"
|
||||||
"goauthentik.io/internal/config"
|
"goauthentik.io/internal/config"
|
||||||
"goauthentik.io/internal/outpost/ak"
|
"goauthentik.io/internal/outpost/ak"
|
||||||
|
"goauthentik.io/internal/outpost/radius/eap/protocol"
|
||||||
"goauthentik.io/internal/outpost/radius/metrics"
|
"goauthentik.io/internal/outpost/radius/metrics"
|
||||||
|
|
||||||
"layeh.com/radius"
|
"layeh.com/radius"
|
||||||
@ -22,23 +23,27 @@ type ProviderInstance struct {
|
|||||||
appSlug string
|
appSlug string
|
||||||
flowSlug string
|
flowSlug string
|
||||||
providerId int32
|
providerId int32
|
||||||
|
certId string
|
||||||
s *RadiusServer
|
s *RadiusServer
|
||||||
log *log.Entry
|
log *log.Entry
|
||||||
|
eapState map[string]*protocol.State
|
||||||
}
|
}
|
||||||
|
|
||||||
type RadiusServer struct {
|
type RadiusServer struct {
|
||||||
s radius.PacketServer
|
s radius.PacketServer
|
||||||
log *log.Entry
|
log *log.Entry
|
||||||
ac *ak.APIController
|
ac *ak.APIController
|
||||||
|
cryptoStore *ak.CryptoStore
|
||||||
|
|
||||||
providers []*ProviderInstance
|
providers map[int32]*ProviderInstance
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewServer(ac *ak.APIController) ak.Outpost {
|
func NewServer(ac *ak.APIController) ak.Outpost {
|
||||||
rs := &RadiusServer{
|
rs := &RadiusServer{
|
||||||
log: log.WithField("logger", "authentik.outpost.radius"),
|
log: log.WithField("logger", "authentik.outpost.radius"),
|
||||||
ac: ac,
|
ac: ac,
|
||||||
providers: []*ProviderInstance{},
|
providers: map[int32]*ProviderInstance{},
|
||||||
|
cryptoStore: ak.NewCryptoStore(ac.Client.CryptoApi),
|
||||||
}
|
}
|
||||||
rs.s = radius.PacketServer{
|
rs.s = radius.PacketServer{
|
||||||
Handler: rs,
|
Handler: rs,
|
||||||
@ -85,7 +90,7 @@ func (rs *RadiusServer) RADIUSSecret(ctx context.Context, remoteAddr net.Addr) (
|
|||||||
return bi < bj
|
return bi < bj
|
||||||
})
|
})
|
||||||
candidate := matchedPrefixes[0]
|
candidate := matchedPrefixes[0]
|
||||||
rs.log.WithField("ip", ip.String()).WithField("cidr", candidate.c.String()).Debug("Matched CIDR")
|
rs.log.WithField("ip", ip.String()).WithField("cidr", candidate.c.String()).WithField("instance", candidate.p.appSlug).Debug("Matched CIDR")
|
||||||
return candidate.p.SharedSecret, nil
|
return candidate.p.SharedSecret, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -98,7 +103,8 @@ func (rs *RadiusServer) Start() error {
|
|||||||
}()
|
}()
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
err := rs.StartRadiusServer()
|
rs.log.WithField("listen", rs.s.Addr).Info("Starting radius server")
|
||||||
|
err := rs.s.ListenAndServe()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic(err)
|
panic(err)
|
||||||
}
|
}
|
||||||
|
4
notes.md
Normal file
4
notes.md
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
|
||||||
|
eapol_test -s foo -a 192.168.68.1 -c config
|
||||||
|
|
||||||
|
sudo tcpdump -i bridge100 port 1812 -w eap.pcap
|
16
schema.yml
16
schema.yml
@ -54849,6 +54849,10 @@ components:
|
|||||||
should only be enabled if all users that will bind to this provider have
|
should only be enabled if all users that will bind to this provider have
|
||||||
a TOTP device configured, as otherwise a password may incorrectly be rejected
|
a TOTP device configured, as otherwise a password may incorrectly be rejected
|
||||||
if it contains a semicolon.
|
if it contains a semicolon.
|
||||||
|
certificate:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
nullable: true
|
||||||
PatchedRedirectStageRequest:
|
PatchedRedirectStageRequest:
|
||||||
type: object
|
type: object
|
||||||
description: RedirectStage Serializer
|
description: RedirectStage Serializer
|
||||||
@ -57302,6 +57306,10 @@ components:
|
|||||||
should only be enabled if all users that will bind to this provider have
|
should only be enabled if all users that will bind to this provider have
|
||||||
a TOTP device configured, as otherwise a password may incorrectly be rejected
|
a TOTP device configured, as otherwise a password may incorrectly be rejected
|
||||||
if it contains a semicolon.
|
if it contains a semicolon.
|
||||||
|
certificate:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
nullable: true
|
||||||
required:
|
required:
|
||||||
- application_slug
|
- application_slug
|
||||||
- auth_flow_slug
|
- auth_flow_slug
|
||||||
@ -57388,6 +57396,10 @@ components:
|
|||||||
should only be enabled if all users that will bind to this provider have
|
should only be enabled if all users that will bind to this provider have
|
||||||
a TOTP device configured, as otherwise a password may incorrectly be rejected
|
a TOTP device configured, as otherwise a password may incorrectly be rejected
|
||||||
if it contains a semicolon.
|
if it contains a semicolon.
|
||||||
|
certificate:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
nullable: true
|
||||||
required:
|
required:
|
||||||
- assigned_application_name
|
- assigned_application_name
|
||||||
- assigned_application_slug
|
- assigned_application_slug
|
||||||
@ -57512,6 +57524,10 @@ components:
|
|||||||
should only be enabled if all users that will bind to this provider have
|
should only be enabled if all users that will bind to this provider have
|
||||||
a TOTP device configured, as otherwise a password may incorrectly be rejected
|
a TOTP device configured, as otherwise a password may incorrectly be rejected
|
||||||
if it contains a semicolon.
|
if it contains a semicolon.
|
||||||
|
certificate:
|
||||||
|
type: string
|
||||||
|
format: uuid
|
||||||
|
nullable: true
|
||||||
required:
|
required:
|
||||||
- authorization_flow
|
- authorization_flow
|
||||||
- invalidation_flow
|
- invalidation_flow
|
||||||
|
@ -1,3 +1,4 @@
|
|||||||
|
import "@goauthentik/admin/common/ak-crypto-certificate-search";
|
||||||
import "@goauthentik/admin/common/ak-flow-search/ak-branded-flow-search";
|
import "@goauthentik/admin/common/ak-flow-search/ak-branded-flow-search";
|
||||||
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
|
import "@goauthentik/admin/common/ak-flow-search/ak-flow-search";
|
||||||
import { ascii_letters, digits, randomString } from "@goauthentik/common/utils";
|
import { ascii_letters, digits, randomString } from "@goauthentik/common/utils";
|
||||||
@ -93,6 +94,14 @@ export function renderForm(
|
|||||||
help=${clientNetworksHelp}
|
help=${clientNetworksHelp}
|
||||||
input-hint="code"
|
input-hint="code"
|
||||||
></ak-text-input>
|
></ak-text-input>
|
||||||
|
<ak-form-element-horizontal label=${msg("Certificate")} name="certificate">
|
||||||
|
<!-- NOTE: 'null' cast to 'undefined' on signingKey to satisfy Lit requirements -->
|
||||||
|
<ak-crypto-certificate-search
|
||||||
|
certificate=${ifDefined(provider?.certificate ?? undefined)}
|
||||||
|
singleton
|
||||||
|
></ak-crypto-certificate-search>
|
||||||
|
<p class="pf-c-form__helper-text">${msg("Certificate used for EAP-TLS.")}</p>
|
||||||
|
</ak-form-element-horizontal>
|
||||||
<ak-form-element-horizontal
|
<ak-form-element-horizontal
|
||||||
label=${msg("Property mappings")}
|
label=${msg("Property mappings")}
|
||||||
name="propertyMappings"
|
name="propertyMappings"
|
||||||
|
Reference in New Issue
Block a user