Initial commit: Import from /home/simon/test/ac

This commit is contained in:
zhangsz
2025-11-05 13:16:01 +08:00
commit b1dc1e18e7
28 changed files with 3471 additions and 0 deletions

46
.gitignore vendored Normal file
View File

@@ -0,0 +1,46 @@
# Git
.git/
.gitattributes
# OS
.DS_Store
Thumbs.db
*.swp
*.swo
*~
# IDE
.idea/
.vscode/
*.sublime-*
# Language specific
node_modules/
__pycache__/
*.pyc
*.pyo
.venv/
venv/
*.egg-info/
.gradle/
target/
dist/
build/
.next/
.nuxt/
# Logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
# Environment
.env
.env.local
.env.*.local
# Temporary
*.tmp
*.temp
*.cache

52
Makefile Executable file
View File

@@ -0,0 +1,52 @@
GO_BIN_PATH = bin
GO_SRC_PATH = src
ROOT_PATH = $(shell pwd)
GO_NF = ac
GO_FILES = $(shell find $(GO_SRC_PATH)/$(%) -name "*.go" ! -name "*_test.go")
VERSION = 1.2408.0
COMMIT_HASH = $(shell git log -1 --format=%h)
COMMIT_TIME = $(shell git log --pretty="@%at" -1 | xargs date -u +"%Y-%m-%d %H:%M:%SZ" -d)
LDFLAGS = -X 'ac/internal/version.VERSION=$(VERSION)' \
-X 'ac/internal/version.COMMIT_HASH=$(COMMIT_HASH)' \
-X 'ac/internal/version.COMMIT_TIME=$(COMMIT_TIME)'
.PHONY: $(GO_NF) clean
.DEFAULT_GOAL: $(GO_NF)
all: $(GO_NF)
debug: GCFLAGS += -N -l
debug: all
$(GO_NF): % : $(GO_BIN_PATH)/%
$(GO_BIN_PATH)/%: $(GO_FILES)
# $(@F): The file-within-directory part of the file name of the target.
@echo "Start building $(@F)...."
cd $(GO_SRC_PATH)/cmd && \
go build -gcflags "$(GCFLAGS)" -ldflags "$(LDFLAGS)" -o $(ROOT_PATH)/$@ main.go
vpath %.go $(addprefix $(GO_SRC_PATH)/, $(GO_NF))
deb:
test -d debian && rm -rf debian/* || mkdir debian
mkdir -p debian/DEBIAN
mkdir -p debian/usr/local/bin
mkdir -p debian/usr/local/etc/ac/default
mkdir -p debian/lib/systemd/system
cp $(GO_BIN_PATH)/$(GO_NF) debian/usr/local/bin
cp config/ac.yaml debian/usr/local/etc/ac/default
cp scripts/ac.service debian/lib/systemd/system
cp scripts/postinst debian/DEBIAN
cp scripts/prerm debian/DEBIAN
cp scripts/control debian/DEBIAN
fakeroot dpkg-deb --build debian
mv debian.deb debian/ac-r$(VERSION)-ub22.deb
clean:
rm -rf $(addprefix $(GO_BIN_PATH)/, $(GO_NF))

2
config/ac.yaml Executable file
View File

@@ -0,0 +1,2 @@
debugLevel: trace
capwapAddr: 192.168.10.158

11
scripts/ac.service Executable file
View File

@@ -0,0 +1,11 @@
[Service]
Type=idle
Environment=GOTRACEBACK=crash
ExecStart=/usr/local/bin/ac
Restart=always
RestartSec=1
StartLimitInterval=0
StandardOutput=null
[Install]
WantedBy=multi-user.target

10
scripts/control Normal file
View File

@@ -0,0 +1,10 @@
Package: ac
Version: 2.2409.0
Section: net
Priority: optional
Architecture: amd64
Essential: no
Depends:
Conflicts: ac
Maintainer: AC maintainer
Description: WLAN Software

15
scripts/postinst Executable file
View File

@@ -0,0 +1,15 @@
#! /bin/bash
service_name=ac
test ! -f /usr/local/etc/ac/ac.yaml && cp -f /usr/local/etc/ac/default/ac.yaml /usr/local/etc/ac
if test -x /sbin/ldconfig
then
/sbin/ldconfig
else
echo Cannot run /sbin/ldconfig
fi
systemctl enable $service_name

7
scripts/prerm Executable file
View File

@@ -0,0 +1,7 @@
#! /bin/bash
# Commands to be run before uninstall of the package
service_name=ac
systemctl disable $service_name

49
src/cmd/main.go Normal file
View File

@@ -0,0 +1,49 @@
package main
import (
"context"
"os"
"os/signal"
"runtime/debug"
"syscall"
"ac/internal/logger"
"ac/internal/version"
"ac/pkg/factory"
"ac/pkg/service"
)
func main() {
defer func() {
if p := recover(); p != nil {
// Print stack for panic to log. Fatalf() will let program exit.
logger.MainLog.Fatalf("panic: %v\n%s", p, string(debug.Stack()))
}
}()
logger.MainLog.Infoln("AC version: ", version.GetVersionAndHash())
ctx, cancel := context.WithCancel(context.Background())
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, os.Interrupt, syscall.SIGTERM)
go func() {
<-sigCh // Wait for interrupt signal to gracefully shutdown
cancel() // Notify each goroutine and wait them stopped
}()
cfg, err := factory.ReadConfig()
if err != nil {
logger.MainLog.Errorf("AC Run error: %v\n", err)
return
}
factory.AcConfig = cfg
ac, err := service.NewApp(ctx, cfg)
if err != nil {
logger.MainLog.Errorf("AC Run error: %v\n", err)
return
}
ac.Start()
}

28
src/go.mod Normal file
View File

@@ -0,0 +1,28 @@
module ac
go 1.23.3
require (
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2
github.com/davecgh/go-spew v1.1.1
github.com/golang/protobuf v1.5.4
github.com/mochi-mqtt/server/v2 v2.6.6
github.com/natefinch/lumberjack v2.0.0+incompatible
github.com/sirupsen/logrus v1.9.3
github.com/tim-ywliu/nested-logrus-formatter v1.3.2
google.golang.org/grpc v1.68.0
google.golang.org/protobuf v1.35.1
gopkg.in/yaml.v2 v2.4.0
)
require (
github.com/BurntSushi/toml v1.4.0 // indirect
github.com/gorilla/websocket v1.5.0 // indirect
github.com/rs/xid v1.4.0 // indirect
golang.org/x/net v0.29.0 // indirect
golang.org/x/sys v0.25.0 // indirect
golang.org/x/text v0.18.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)

53
src/go.sum Normal file
View File

@@ -0,0 +1,53 @@
github.com/BurntSushi/toml v1.4.0 h1:kuoIxZQy2WRRk1pttg9asf+WVv6tWQuBNVmK8+nqPr0=
github.com/BurntSushi/toml v1.4.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2 h1:DklsrG3dyBCFEj5IhUbnKptjxatkF07cF2ak3yi77so=
github.com/asaskevich/govalidator v0.0.0-20230301143203-a9d515a09cc2/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/jinzhu/copier v0.3.5 h1:GlvfUwHk62RokgqVNvYsku0TATCF7bAHVwEXoBh3iJg=
github.com/jinzhu/copier v0.3.5/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
github.com/mochi-mqtt/server/v2 v2.6.6 h1:FmL5ebeIIA+AKo/nX0DF8Yc2MMWFLQCwh3FZBEmg6dQ=
github.com/mochi-mqtt/server/v2 v2.6.6/go.mod h1:TqztjKGO0/ArOjJt9x9idk0kqPT3CVN8Pb+l+PS5Gdo=
github.com/natefinch/lumberjack v2.0.0+incompatible h1:4QJd3OLAMgj7ph+yZTuX13Ld4UpgHp07nNdFX7mqFfM=
github.com/natefinch/lumberjack v2.0.0+incompatible/go.mod h1:Wi9p2TTF5DG5oU+6YfsmYQpsTIOm0B1VNzQg9Mw6nPk=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/rs/xid v1.4.0 h1:qd7wPTDkN6KQx2VmMBLrpHkiyQwgFXRnkOLacUiaSNY=
github.com/rs/xid v1.4.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ=
github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/tim-ywliu/nested-logrus-formatter v1.3.2 h1:jugNJ2/CNCI79SxOJCOhwUHeN3O7/7/bj+ZRGOFlCSw=
github.com/tim-ywliu/nested-logrus-formatter v1.3.2/go.mod h1:oGPmcxZB65j9Wo7mCnQKSrKEJtVDqyjD666SGmyStXI=
golang.org/x/net v0.29.0 h1:5ORfpBpCs4HzDYoodCDBbwHzdR5UrLBZ3sOnUJmFoHo=
golang.org/x/net v0.29.0/go.mod h1:gLkgy8jTGERgjzMic6DS9+SP0ajcu6Xu3Orq/SpETg0=
golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.25.0 h1:r+8e+loiHxRqhXVl6ML1nO3l1+oFoWbnlu2Ehimmi34=
golang.org/x/sys v0.25.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/text v0.18.0 h1:XvMDiNzPAl0jr17s6W9lcaIhGUfUORdGCNsuLmPG224=
golang.org/x/text v0.18.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
google.golang.org/grpc v1.68.0 h1:aHQeeJbo8zAkAa3pRzrVjZlbz6uSfeOXlJNQM0RAbz0=
google.golang.org/grpc v1.68.0/go.mod h1:fmSPC5AsjSBCK54MyHRx48kpOti1/jRfOlwEWywNjWA=
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=

View File

@@ -0,0 +1,17 @@
package capwap
import (
"net"
capwap_service "ac/internal/capwap/service"
"ac/internal/logger"
)
func HandleMessage(peerAddr net.Addr, msg []byte) {
if len(msg) > 0 {
discoverResponse := [...]byte{0x00, 0x00, 0x00, 0x01, 0x00, 0x00, 0x00, 0x20, 0x0f, 0xaa, 0x92, 0xab, 0x5a, 0x4b, 0x31, 0x30, 0x35, 0x30, 0x46, 0x30, 0x32, 0x34, 0x38, 0x31, 0x33, 0x30, 0x30, 0x30, 0x32, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x7f, 0x8c, 0xbf, 0x64, 0xb1, 0x46, 0xd1, 0x64, 0xcf, 0x88, 0x6d, 0x28, 0x47, 0x12, 0xa1, 0x75, 0xdb, 0xbe, 0x1d, 0x35, 0x0b, 0xfa, 0xa6, 0xa7, 0x2c, 0x36, 0x89, 0x27, 0x7f, 0x98, 0xff, 0xa6}
if _, err := capwap_service.SendMsg(peerAddr, discoverResponse[:]); err != nil {
logger.CapwapLog.Errorf("Capwap send Discovery Response error : %s", err)
}
}
}

View File

@@ -0,0 +1,60 @@
package service
import (
"encoding/hex"
"net"
"ac/internal/logger"
)
type Handler func(addr net.Addr, msg []byte)
var pktConn net.PacketConn
func Run(address string, msgHandler Handler) {
var err error
laddr, err := net.ResolveUDPAddr("udp", "["+address+"]:5246")
if err != nil {
logger.CapwapLog.Println(err)
return
}
// setup underlying connection first.
// not using net.Dial, as it binds src/dst IP:Port, which makes it harder to
// handle multiple connections with a Conn.
pktConn, err = net.ListenPacket(laddr.Network(), laddr.String())
if err != nil {
return
}
go func() {
for {
buf := make([]byte, 4096)
n, addr, err := pktConn.ReadFrom(buf)
if err != nil {
return
}
logger.CapwapLog.Tracef("Read %d bytes", n)
logger.CapwapLog.Tracef("Packet content:\n%+v", hex.Dump(buf[:n]))
msgHandler(addr, buf[:n])
}
}()
}
// SendMsg - used to send out message to UDP connection
func SendMsg(raddr net.Addr, msg []byte) (int, error) {
if pktConn == nil || raddr == nil {
logger.CapwapLog.Warn("SendMsg failed.")
return 0, nil
}
return pktConn.WriteTo(msg, raddr)
}
func Stop() {
if pktConn != nil {
pktConn.Close()
}
}

View File

@@ -0,0 +1,74 @@
package context
import (
"sync"
mqtt_server "github.com/mochi-mqtt/server/v2"
"ac/internal/mqtt/message"
"ac/pkg/factory"
)
var acContext ACContext
func init() {
}
type ACContext struct {
ApPool sync.Map // map[mqtt_server.Client]*Ap
CapwapAddr string
}
type Ap struct {
Client *mqtt_server.Client
LastEcho *message.Echo
}
func InitAcContext(context *ACContext) {
config := factory.AcConfig
context.CapwapAddr = config.CapwapAddr
}
func (context *ACContext) NewAp(client *mqtt_server.Client) *Ap {
ap := Ap{Client: client}
context.ApPool.Store(client, &ap)
return &ap
}
// use mqtt_server.Client to find AP context, return *Ap and ok bit
func (context *ACContext) ApFindByClient(client *mqtt_server.Client) (*Ap, bool) {
if value, ok := context.ApPool.Load(client); ok {
return value.(*Ap), ok
}
return nil, false
}
// use clientId to find AP context, return *Ap and ok bit
func (context *ACContext) ApFindByClientId(clientId string) (ap *Ap, ok bool) {
context.ApPool.Range(func(key, value interface{}) bool {
candidate := value.(*Ap)
if ok = (candidate.Client.ID == clientId); ok {
ap = candidate
return false
}
return true
})
return
}
func (context *ACContext) DeleteAp(client *mqtt_server.Client) {
context.ApPool.Delete(client)
}
// Reset Ac Context
func (context *ACContext) Reset() {
context.ApPool.Range(func(key, value interface{}) bool {
context.ApPool.Delete(key)
return true
})
}
// Create new AC context
func GetSelf() *ACContext {
return &acContext
}

View File

@@ -0,0 +1,189 @@
// Code generated by protoc-gen-go. DO NOT EDIT.
// versions:
// protoc-gen-go v1.34.2-devel
// protoc v3.12.4
// source: dhcp_server.proto
package dhcpServer
import (
protoreflect "google.golang.org/protobuf/reflect/protoreflect"
protoimpl "google.golang.org/protobuf/runtime/protoimpl"
reflect "reflect"
sync "sync"
)
const (
// Verify that this generated code is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(20 - protoimpl.MinVersion)
// Verify that runtime/protoimpl is sufficiently up-to-date.
_ = protoimpl.EnforceVersion(protoimpl.MaxVersion - 20)
)
type MacAddr struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Mac string `protobuf:"bytes,1,opt,name=mac,proto3" json:"mac,omitempty"`
}
func (x *MacAddr) Reset() {
*x = MacAddr{}
mi := &file_dhcp_server_proto_msgTypes[0]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *MacAddr) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*MacAddr) ProtoMessage() {}
func (x *MacAddr) ProtoReflect() protoreflect.Message {
mi := &file_dhcp_server_proto_msgTypes[0]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use MacAddr.ProtoReflect.Descriptor instead.
func (*MacAddr) Descriptor() ([]byte, []int) {
return file_dhcp_server_proto_rawDescGZIP(), []int{0}
}
func (x *MacAddr) GetMac() string {
if x != nil {
return x.Mac
}
return ""
}
// The response message containing the staInfo.
type StaDhcpInfo struct {
state protoimpl.MessageState
sizeCache protoimpl.SizeCache
unknownFields protoimpl.UnknownFields
Ip string `protobuf:"bytes,1,opt,name=ip,proto3" json:"ip,omitempty"`
Hostname string `protobuf:"bytes,2,opt,name=hostname,proto3" json:"hostname,omitempty"`
}
func (x *StaDhcpInfo) Reset() {
*x = StaDhcpInfo{}
mi := &file_dhcp_server_proto_msgTypes[1]
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
ms.StoreMessageInfo(mi)
}
func (x *StaDhcpInfo) String() string {
return protoimpl.X.MessageStringOf(x)
}
func (*StaDhcpInfo) ProtoMessage() {}
func (x *StaDhcpInfo) ProtoReflect() protoreflect.Message {
mi := &file_dhcp_server_proto_msgTypes[1]
if x != nil {
ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x))
if ms.LoadMessageInfo() == nil {
ms.StoreMessageInfo(mi)
}
return ms
}
return mi.MessageOf(x)
}
// Deprecated: Use StaDhcpInfo.ProtoReflect.Descriptor instead.
func (*StaDhcpInfo) Descriptor() ([]byte, []int) {
return file_dhcp_server_proto_rawDescGZIP(), []int{1}
}
func (x *StaDhcpInfo) GetIp() string {
if x != nil {
return x.Ip
}
return ""
}
func (x *StaDhcpInfo) GetHostname() string {
if x != nil {
return x.Hostname
}
return ""
}
var File_dhcp_server_proto protoreflect.FileDescriptor
var file_dhcp_server_proto_rawDesc = []byte{
0x0a, 0x11, 0x64, 0x68, 0x63, 0x70, 0x5f, 0x73, 0x65, 0x72, 0x76, 0x65, 0x72, 0x2e, 0x70, 0x72,
0x6f, 0x74, 0x6f, 0x22, 0x1b, 0x0a, 0x07, 0x4d, 0x61, 0x63, 0x41, 0x64, 0x64, 0x72, 0x12, 0x10,
0x0a, 0x03, 0x6d, 0x61, 0x63, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x03, 0x6d, 0x61, 0x63,
0x22, 0x39, 0x0a, 0x0b, 0x53, 0x74, 0x61, 0x44, 0x68, 0x63, 0x70, 0x49, 0x6e, 0x66, 0x6f, 0x12,
0x0e, 0x0a, 0x02, 0x69, 0x70, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x02, 0x69, 0x70, 0x12,
0x1a, 0x0a, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x02, 0x20, 0x01, 0x28,
0x09, 0x52, 0x08, 0x68, 0x6f, 0x73, 0x74, 0x6e, 0x61, 0x6d, 0x65, 0x32, 0x30, 0x0a, 0x0a, 0x53,
0x74, 0x61, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x22, 0x0a, 0x06, 0x47, 0x65, 0x74,
0x53, 0x74, 0x61, 0x12, 0x08, 0x2e, 0x4d, 0x61, 0x63, 0x41, 0x64, 0x64, 0x72, 0x1a, 0x0c, 0x2e,
0x53, 0x74, 0x61, 0x44, 0x68, 0x63, 0x70, 0x49, 0x6e, 0x66, 0x6f, 0x22, 0x00, 0x42, 0x0e, 0x5a,
0x0c, 0x2e, 0x2f, 0x64, 0x68, 0x63, 0x70, 0x53, 0x65, 0x72, 0x76, 0x65, 0x72, 0x62, 0x06, 0x70,
0x72, 0x6f, 0x74, 0x6f, 0x33,
}
var (
file_dhcp_server_proto_rawDescOnce sync.Once
file_dhcp_server_proto_rawDescData = file_dhcp_server_proto_rawDesc
)
func file_dhcp_server_proto_rawDescGZIP() []byte {
file_dhcp_server_proto_rawDescOnce.Do(func() {
file_dhcp_server_proto_rawDescData = protoimpl.X.CompressGZIP(file_dhcp_server_proto_rawDescData)
})
return file_dhcp_server_proto_rawDescData
}
var file_dhcp_server_proto_msgTypes = make([]protoimpl.MessageInfo, 2)
var file_dhcp_server_proto_goTypes = []any{
(*MacAddr)(nil), // 0: MacAddr
(*StaDhcpInfo)(nil), // 1: StaDhcpInfo
}
var file_dhcp_server_proto_depIdxs = []int32{
0, // 0: StaService.GetSta:input_type -> MacAddr
1, // 1: StaService.GetSta:output_type -> StaDhcpInfo
1, // [1:2] is the sub-list for method output_type
0, // [0:1] is the sub-list for method input_type
0, // [0:0] is the sub-list for extension type_name
0, // [0:0] is the sub-list for extension extendee
0, // [0:0] is the sub-list for field type_name
}
func init() { file_dhcp_server_proto_init() }
func file_dhcp_server_proto_init() {
if File_dhcp_server_proto != nil {
return
}
type x struct{}
out := protoimpl.TypeBuilder{
File: protoimpl.DescBuilder{
GoPackagePath: reflect.TypeOf(x{}).PkgPath(),
RawDescriptor: file_dhcp_server_proto_rawDesc,
NumEnums: 0,
NumMessages: 2,
NumExtensions: 0,
NumServices: 1,
},
GoTypes: file_dhcp_server_proto_goTypes,
DependencyIndexes: file_dhcp_server_proto_depIdxs,
MessageInfos: file_dhcp_server_proto_msgTypes,
}.Build()
File_dhcp_server_proto = out.File
file_dhcp_server_proto_rawDesc = nil
file_dhcp_server_proto_goTypes = nil
file_dhcp_server_proto_depIdxs = nil
}

View File

@@ -0,0 +1,18 @@
syntax = "proto3";
option go_package = "./dhcpServer";
// The get service definition.
service StaService {
rpc GetSta(MacAddr) returns (StaDhcpInfo) {}
}
message MacAddr {
string mac = 1;
}
// The response message containing the staInfo.
message StaDhcpInfo {
string ip = 1;
string hostname = 2;
}

View File

@@ -0,0 +1,125 @@
// Code generated by protoc-gen-go-grpc. DO NOT EDIT.
// versions:
// - protoc-gen-go-grpc v1.5.1
// - protoc v3.12.4
// source: dhcp_server.proto
package dhcpServer
import (
context "context"
grpc "google.golang.org/grpc"
codes "google.golang.org/grpc/codes"
status "google.golang.org/grpc/status"
)
// This is a compile-time assertion to ensure that this generated file
// is compatible with the grpc package it is being compiled against.
// Requires gRPC-Go v1.64.0 or later.
const _ = grpc.SupportPackageIsVersion9
const (
StaService_GetSta_FullMethodName = "/StaService/GetSta"
)
// StaServiceClient is the client API for StaService service.
//
// For semantics around ctx use and closing/ending streaming RPCs, please refer to https://pkg.go.dev/google.golang.org/grpc/?tab=doc#ClientConn.NewStream.
//
// The get service definition.
type StaServiceClient interface {
GetSta(ctx context.Context, in *MacAddr, opts ...grpc.CallOption) (*StaDhcpInfo, error)
}
type staServiceClient struct {
cc grpc.ClientConnInterface
}
func NewStaServiceClient(cc grpc.ClientConnInterface) StaServiceClient {
return &staServiceClient{cc}
}
func (c *staServiceClient) GetSta(ctx context.Context, in *MacAddr, opts ...grpc.CallOption) (*StaDhcpInfo, error) {
cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...)
out := new(StaDhcpInfo)
err := c.cc.Invoke(ctx, StaService_GetSta_FullMethodName, in, out, cOpts...)
if err != nil {
return nil, err
}
return out, nil
}
// StaServiceServer is the server API for StaService service.
// All implementations must embed UnimplementedStaServiceServer
// for forward compatibility.
//
// The get service definition.
type StaServiceServer interface {
GetSta(context.Context, *MacAddr) (*StaDhcpInfo, error)
mustEmbedUnimplementedStaServiceServer()
}
// UnimplementedStaServiceServer must be embedded to have
// forward compatible implementations.
//
// NOTE: this should be embedded by value instead of pointer to avoid a nil
// pointer dereference when methods are called.
type UnimplementedStaServiceServer struct{}
func (UnimplementedStaServiceServer) GetSta(context.Context, *MacAddr) (*StaDhcpInfo, error) {
return nil, status.Errorf(codes.Unimplemented, "method GetSta not implemented")
}
func (UnimplementedStaServiceServer) mustEmbedUnimplementedStaServiceServer() {}
func (UnimplementedStaServiceServer) testEmbeddedByValue() {}
// UnsafeStaServiceServer may be embedded to opt out of forward compatibility for this service.
// Use of this interface is not recommended, as added methods to StaServiceServer will
// result in compilation errors.
type UnsafeStaServiceServer interface {
mustEmbedUnimplementedStaServiceServer()
}
func RegisterStaServiceServer(s grpc.ServiceRegistrar, srv StaServiceServer) {
// If the following call panics, it indicates UnimplementedStaServiceServer was
// embedded by pointer and is nil. This will cause panics if an
// unimplemented method is ever invoked, so we test this at initialization
// time to prevent it from happening at runtime later due to I/O.
if t, ok := srv.(interface{ testEmbeddedByValue() }); ok {
t.testEmbeddedByValue()
}
s.RegisterService(&StaService_ServiceDesc, srv)
}
func _StaService_GetSta_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) {
in := new(MacAddr)
if err := dec(in); err != nil {
return nil, err
}
if interceptor == nil {
return srv.(StaServiceServer).GetSta(ctx, in)
}
info := &grpc.UnaryServerInfo{
Server: srv,
FullMethod: StaService_GetSta_FullMethodName,
}
handler := func(ctx context.Context, req interface{}) (interface{}, error) {
return srv.(StaServiceServer).GetSta(ctx, req.(*MacAddr))
}
return interceptor(ctx, in, info, handler)
}
// StaService_ServiceDesc is the grpc.ServiceDesc for StaService service.
// It's only intended for direct use with grpc.RegisterService,
// and not to be introspected or modified (even as a copy)
var StaService_ServiceDesc = grpc.ServiceDesc{
ServiceName: "StaService",
HandlerType: (*StaServiceServer)(nil),
Methods: []grpc.MethodDesc{
{
MethodName: "GetSta",
Handler: _StaService_GetSta_Handler,
},
},
Streams: []grpc.StreamDesc{},
Metadata: "dhcp_server.proto",
}

View File

@@ -0,0 +1,38 @@
package grpcClient
import (
"context"
"fmt"
"google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
"ac/internal/grpc_client/dhcpServer"
"ac/internal/logger"
)
var client dhcpServer.StaServiceClient
func ConnectToServer(address string, port int) {
target := fmt.Sprintf("%s:%d", address, port)
logger.GrpcLog.Infoln("connecting to target", target)
var err error
conn, err := grpc.NewClient(target, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
logger.GrpcLog.Errorln("did not connect:", err)
return
}
client = dhcpServer.NewStaServiceClient(conn)
}
func GetStaInfo(mac string) (string, string) {
macAddr := dhcpServer.MacAddr{Mac: mac}
staInfo, _ := client.GetSta(context.Background(), &macAddr)
if staInfo != nil {
return staInfo.Ip, staInfo.Hostname
}
return "-", "-"
}

View File

@@ -0,0 +1,64 @@
package logger
import (
"fmt"
"path"
"runtime"
"github.com/natefinch/lumberjack"
"github.com/sirupsen/logrus"
formatter "github.com/tim-ywliu/nested-logrus-formatter"
)
var (
Log *logrus.Logger
MainLog *logrus.Entry
InitLog *logrus.Entry
CfgLog *logrus.Entry
CtxLog *logrus.Entry
CapwapLog *logrus.Entry
MqttLog *logrus.Entry
GrpcLog *logrus.Entry
)
const (
FieldApAddr string = "ap_addr"
)
func init() {
Log = logrus.New()
Log.SetReportCaller(true)
Log.SetOutput(&lumberjack.Logger{
Filename: "/var/log/ac.log",
MaxSize: 200,
MaxBackups: 9,
LocalTime: true,
})
Log.SetFormatter(&formatter.Formatter{
FieldsOrder: []string{"CAT"},
TimestampFormat: "2006-01-02 15:04:05.000",
TrimMessages: true,
NoColors: true,
NoFieldsColors: true,
NoFieldsSpace: true,
HideKeys: true,
CallerFirst: true,
CustomCallerFormatter: func(f *runtime.Frame) string {
file := path.Base(f.File)
if len(file) > 15 {
file = file[len(file)-15:]
}
return fmt.Sprintf(" %15s:%04d", file, f.Line)
},
})
MainLog = Log.WithField("CAT", "Main")
InitLog = Log.WithField("CAT", "Init")
CfgLog = Log.WithField("CAT", "CFG")
CtxLog = Log.WithField("CAT", "CTX")
CapwapLog = Log.WithField("CAT", "Capwap")
MqttLog = Log.WithField("CAT", "MQTT")
GrpcLog = Log.WithField("CAT", "gRPC")
}

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,190 @@
syntax = "proto2";
option go_package = "./message";
enum APMode {
FIT_AP = 1;
FAT_AP = 2; /* soho */
CPE_BASE = 3; /* CPE 基站*/
CPE_STA = 4; /* CPE 接收端*/
}
enum RadioBand {
RB_2G = 1;
RB_5G = 2;
}
enum RadioHTMode {
RHT_20 = 1;
RHT_40 = 2;
RHT_40Minus = 3;
RHT_40Plus = 4;
RHT_80 = 5;
RHT_160 = 6;
RHT_160Plus = 7;
}
enum CMDType {
REBOOT = 1;
UPGRADE = 2;
SETACADDR = 4;
/* 解绑AP 和AC AP 和AC 的绑定关系由配置下发时确定*/
UNBIND = 5;
/* 下线用户支持*/
KICK_USER = 6;
/* 配置设备名称*/
SETHOSTNAME = 7;
/* 没有绑定的时候要求AP 断开和AC 的连接重启查询AC 的流程*/
KICK_AP = 8;
/* 启动扫描*/
START_SCAN = 9;
/* 下发认证*/
AUTH = 10;
/* 下线用户*/
LOGOUT = 11;
/* 下线用户*/
REBOOTAP = 12;
SHELL = 100;
}
message WanConfig {
required string ipproto = 1;
optional string ip = 2;
optional string netmask = 3;
optional int32 metric = 4;
optional string gateway = 5;
optional string dns1 = 6;
optional string dns2 = 7;
}
message WlanConfig
{
required RadioBand band = 1;
required string ssid = 2;
required int32 gbk_enable = 3 [default = 0];
required string encryption = 4;
optional string key = 5;
required int32 disabled = 6 [default = 0];
required int32 vlan = 10 [default = 0];
required int32 maxsta = 11 [default = 0];
required int32 rejrssi = 12 [default = -85];
required int32 wmm = 13 [default = 1];
required int32 isolate = 14 [default = 0];
required int32 hide = 15 [default = 0];
required int32 ieee80211r = 22 [default = 0];
optional int32 auth_type= 25; /* 认证类型,将认证方式和ssid 关联*/
}
message PingWatchdog {
required int32 enable = 1;
optional string target = 2;
optional int32 ping_interval = 3;
optional int32 ping_failures = 4;
optional int32 ping_timeout = 5;
optional int32 ping_watchdog_action = 6 [default = 3];
}
message CMD {
required CMDType type = 1;
optional string args = 2;
}
message Echo {
required string sn = 1;
required string product_name = 2;
/* 标识设备的唯一MAC */
optional string mac = 3;
optional string board = 4;
optional string hostname = 5;
/* 运行时间*/
optional string uptime = 6;
optional uint64 uptime_sec = 61;
optional string version = 7;
required APMode apmode = 8 [default = FIT_AP];
/* 是否是第一次连接(要求下发配置) */
required int32 newconnect = 9 [default = 0];
/* 关键配置的MD5 值*/
optional string apnetwork_md5 = 10;
optional string country = 11;
/* CPU 占用率*/
optional string cpu = 12;
/* 是否云端管理的*/
optional int32 is_on_cloud = 13 [default = 0];
optional string username = 14;
/* 通过设备的上下行总流量统计*/
optional uint64 uploadspeed = 21;
optional uint64 downloadspeed = 22;
optional uint64 uploadbytes = 23;
optional uint64 downloadbytes = 24;
required string acaddr = 25;
required ManageInterface mif = 50; /* 接口信息*/
repeated RadioInfo radioinfo = 51; /* 射频信息*/
optional WanConfig lanconfig = 52; /* lan 口配置*/
optional PingWatchdog pingwatchdog = 53; /* Ping 看门狗*/
repeated WlanConfig ssids = 54; /* ssid 配置*/
optional int32 iptvSupport = 70;
optional int32 iptvEnable = 71;
/* 假设所有的CPE 都是单频的单频2.4 或单频5G */
/* 增加一个CPE 专用的信息上报,信道列表*/
optional string cpeChannelsJson = 100;
}
message StaInfo {
required string mac = 2;
optional string ip = 3;
required int32 signal = 4;
required int32 noise = 5;
required int32 snr = 6;
required string txrate = 7;
required string rxrate = 8;
}
message WlanInfo {
optional string ssid = 1;
repeated StaInfo stas = 2;
}
message RadioInfo {
required RadioBand band = 1;
repeated WlanInfo wlaninfo = 2;
/* 基础三属性*/
required RadioHTMode htmode = 3 [default = RHT_20];
required uint32 txpower = 4 [default = 0];
required uint32 channel = 5 [default = 0];
/* 其他属性*/
optional int32 signal = 20;
optional int32 noise = 21;
optional string bitrate = 22;
/* 增加字段*/
optional int32 maxsta = 23 [default = 0];
optional int32 rejrssi = 24 [default = -85];
optional string country = 25;
optional int32 enable_fils = 26 [default = 1];
optional string mac = 27;
}
message ManageInterface {
required string ifname = 1;
optional string ip = 2;
optional string mac = 3;
optional string netmask = 4;
optional string gateway = 5;
optional string dns1= 6;
optional string dns2= 7;
optional string ipproto = 8; // dhcp static pppoe
}

222
src/internal/mqtt/server.go Normal file
View File

@@ -0,0 +1,222 @@
package mqtt
import (
"bytes"
"fmt"
"log/slog"
"github.com/golang/protobuf/proto"
mqtt_server "github.com/mochi-mqtt/server/v2"
"github.com/mochi-mqtt/server/v2/hooks/auth"
"github.com/mochi-mqtt/server/v2/listeners"
"github.com/mochi-mqtt/server/v2/packets"
"ac/internal/context"
"ac/internal/logger"
"ac/internal/mqtt/message"
"ac/pkg/app"
)
type Server struct {
app.App
mqttServer *mqtt_server.Server
}
var mqttServer *mqtt_server.Server
func NewServer(ac app.App) (*Server, error) {
server := mqtt_server.New(&mqtt_server.Options{
Logger: slog.New(slog.NewTextHandler(logger.Log.Out, &slog.HandlerOptions{
Level: slog.LevelDebug,
})),
InlineClient: true, // you must enable inline client to use direct publishing and subscribing.
})
_ = server.AddHook(new(auth.AllowHook), nil)
tcp := listeners.NewTCP(listeners.Config{
ID: "t1",
Address: ":5247",
})
err := server.AddListener(tcp)
if err != nil {
logger.MqttLog.Fatal(err)
}
// Add ac hook (AcHook) to the server
err = server.AddHook(new(AcHook), &AcHookOptions{
Server: server,
})
if err != nil {
logger.MqttLog.Fatal(err)
}
mqttServer = server
return &Server{ac, server}, nil
}
// Options contains configuration settings for the hook.
type AcHookOptions struct {
Server *mqtt_server.Server
}
type AcHook struct {
mqtt_server.HookBase
config *AcHookOptions
}
func (h *AcHook) ID() string {
return "events-ac"
}
func (h *AcHook) Provides(b byte) bool {
return bytes.Contains([]byte{
mqtt_server.OnConnect,
mqtt_server.OnDisconnect,
mqtt_server.OnSubscribed,
mqtt_server.OnUnsubscribed,
mqtt_server.OnPublished,
mqtt_server.OnPublish,
}, []byte{b})
}
func (h *AcHook) Init(config any) error {
h.Log.Info("initialised")
if _, ok := config.(*AcHookOptions); !ok && config != nil {
return mqtt_server.ErrInvalidConfigType
}
h.config = config.(*AcHookOptions)
if h.config.Server == nil {
return mqtt_server.ErrInvalidConfigType
}
return nil
}
// subscribeCallback handles messages for subscribed topics
func (h *AcHook) subscribeCallback(cl *mqtt_server.Client, sub packets.Subscription, pk packets.Packet) {
h.Log.Info("hook subscribed message", "client", cl.ID, "topic", pk.TopicName)
}
func (h *AcHook) OnConnect(cl *mqtt_server.Client, pk packets.Packet) error {
h.Log.Info("client connected", "client", cl.ID)
// Example demonstrating how to subscribe to a topic within the hook.
h.config.Server.Subscribe("hook/direct/publish", 1, h.subscribeCallback)
// Example demonstrating how to publish a message within the hook
err := h.config.Server.Publish("hook/direct/publish", []byte("packet hook message"), false, 0)
if err != nil {
h.Log.Error("hook.publish", "error", err)
}
acSelf := context.GetSelf()
ap, ok := acSelf.ApFindByClientId(cl.ID)
if ok {
acSelf.DeleteAp(ap.Client)
}
logger.MqttLog.Infof("Create a new Client for: %s", cl.ID)
ap = acSelf.NewAp(cl)
return nil
}
func (h *AcHook) OnDisconnect(cl *mqtt_server.Client, err error, expire bool) {
if err != nil {
h.Log.Info("client disconnected", "client", cl.ID, "expire", expire, "error", err)
} else {
h.Log.Info("client disconnected", "client", cl.ID, "expire", expire)
}
context.GetSelf().DeleteAp(cl)
}
func (h *AcHook) OnSubscribed(cl *mqtt_server.Client, pk packets.Packet, reasonCodes []byte) {
h.Log.Info(fmt.Sprintf("subscribed qos=%v", reasonCodes), "client", cl.ID, "filters", pk.Filters)
}
func (h *AcHook) OnUnsubscribed(cl *mqtt_server.Client, pk packets.Packet) {
h.Log.Info("unsubscribed", "client", cl.ID, "filters", pk.Filters)
}
func (h *AcHook) OnPublish(cl *mqtt_server.Client, pk packets.Packet) (packets.Packet, error) {
h.Log.Info("received from client", "client", cl.ID, "payload", string(pk.Payload))
if cl.ID == "inline" {
return pk, nil
}
acSelf := context.GetSelf()
ap, ok := acSelf.ApFindByClient(cl)
if !ok {
logger.MqttLog.Infof("Create a new Client for: %s", cl.ID)
ap = acSelf.NewAp(cl)
}
if pk.TopicName == "AP/echo" {
var unmarshaledEcho message.Echo
err := proto.Unmarshal(pk.Payload, &unmarshaledEcho)
if err != nil {
logger.MqttLog.Errorf("Unmarshal to struct error: %v", err)
} else {
logger.MqttLog.Infof("received echo: %+v", &unmarshaledEcho)
ap.LastEcho = &unmarshaledEcho
}
}
return pk, nil
}
func (h *AcHook) OnPublished(cl *mqtt_server.Client, pk packets.Packet) {
h.Log.Info("published to client", "client", cl.ID, "payload", string(pk.Payload))
}
func SendCmdReboot(apSn string) {
var cmd message.CMD
cmd.Type = new(message.CMDType)
*cmd.Type = message.CMDType_REBOOT
encoded, err := proto.Marshal(&cmd)
if err != nil {
logger.MqttLog.Errorf("Encode to protobuf data error: %v", err)
} else {
topic := "AC/" + apSn + "/M_cmd"
err := mqttServer.Publish(topic, encoded, false, 2)
if err != nil {
logger.MqttLog.Error("publish", "error", err)
}
}
}
func SendCmdKickUser(apSn, staMac string) {
var cmd message.CMD
cmd.Type = new(message.CMDType)
*cmd.Type = message.CMDType_KICK_USER
cmd.Args = new(string)
*cmd.Args = staMac
encoded, err := proto.Marshal(&cmd)
if err != nil {
logger.MqttLog.Errorf("Encode to protobuf data error: %v", err)
} else {
topic := "AC/" + apSn + "/M_cmd"
err := mqttServer.Publish(topic, encoded, false, 2)
if err != nil {
logger.MqttLog.Error("publish", "error", err)
}
}
}
func (s *Server)Run() error {
logger.MqttLog.Infof("Start MQTT server")
return s.mqttServer.Serve()
}
func (s *Server)Stop() {
if s.mqttServer != nil {
logger.MqttLog.Infof("Stop MQTT server")
if err := s.mqttServer.Close(); err != nil {
logger.MqttLog.Errorf("Could not close MQTT server: %#v", err)
}
}
}

147
src/internal/telnet/handler.go Executable file
View File

@@ -0,0 +1,147 @@
package telnet
import (
"fmt"
"time"
"ac/internal/context"
grpcClient "ac/internal/grpc_client"
"ac/internal/mqtt"
"ac/internal/mqtt/message"
"ac/internal/version"
)
const prompt string = "AC> "
const help_menu string =
"\033[2J\033[1;1HAVAILABLE COMMANDS\n" +
"==============================================================================\n" +
"| Command | Remark |\n" +
"==============================================================================\n" +
"| help | Help page. |\n" +
"| date | Current date. |\n" +
"| list ver | Display version information. |\n" +
"| list ap | List all APs. |\n" +
"| list subs | List all wifi subscriber data. |\n" +
"| kick #apSn #staMac | Kick the STA specified by mac. |\n" +
"| reboot #apSn | Reboot the AP specified by SN. |\n" +
"| debug level error/warn/info/debug/trace | Set debug level. |\n" +
"| q or quit | Quit. |\n" +
"==============================================================================\n"
func HandleTelnetRequest(cmd []string) string {
var response string
var opr, obj, val string
if len(cmd) > 0 {
opr = cmd[0]
if len(cmd) > 1 {
obj = cmd[1]
if len(cmd) > 2 {
val = cmd[2]
}
}
}
switch opr {
case "date":
response = time.Now().String() + "\n"
case "list":
switch obj {
case "ver":
response = version.GetVersionInfo() + "\n"
case "ap":
response = list_ap()
case "subs":
response = list_subs()
/*case "mac":
response = list_mac(val)*/
default:
response = "COMMAND NOT FOUND\n"
}
case "kick":
mqtt.SendCmdKickUser(obj, val)
response = "COMMAND OK\n"
case "reboot":
mqtt.SendCmdReboot(obj)
response = "COMMAND OK\n"
default:
response = "COMMAND NOT FOUND\n"
}
return response
}
func list_ap() string {
response := fmt.Sprintf("%-5s %-2s %-14s %-4s %-4s %-13s %-15s %-3s\n", "型号", "设备名称", "序列号", "工作模式", "接入方式", "IP地址", "MAC地址", "已运行")
context.GetSelf().ApPool.Range(func(key, value interface{}) bool {
ap := value.(*context.Ap)
if ap.LastEcho != nil {
response += fmt.Sprintf("%-7s %-8s %-17s %-8s %-8s %-15s %-17s %-6s\n",
ap.LastEcho.GetProductName(), ap.LastEcho.GetHostname(), ap.LastEcho.GetSn(), message.APMode_name[int32(ap.LastEcho.GetApmode())],
ap.LastEcho.Mif.GetIpproto(), ap.LastEcho.Mif.GetIp(), ap.LastEcho.Mif.GetMac(), ap.LastEcho.GetUptime())
}
return true
})
return response
}
func list_subs() string {
response := fmt.Sprintf("%-13s %-15s %-8s %-2s %-15s %-6s %-17s %-7s %-4s %-4s %-4s\n", "IP地址", "MAC地址", "主机名", "频段", "所属AP", "所属AP名称", "AP MAC", "无线名称", "信号强度", "接收速率", "发射速率")
context.GetSelf().ApPool.Range(func(key, value interface{}) bool {
ap := value.(*context.Ap)
if ap.LastEcho != nil {
response += buildStaInfo(ap.LastEcho)
}
return true
})
return response
}
func buildStaInfo(echo *message.Echo) string {
var response string
sn := echo.GetSn() // 所属AP
apname := echo.GetHostname() // 所属AP名称
apmac := echo.GetMac() // AP MAC
radioinfos := echo.GetRadioinfo()
if radioinfos == nil {
return ""
}
for _, radioinfo := range radioinfos {
var band string
switch radioinfo.GetBand() {
case message.RadioBand_RB_2G:
band = "2.4G"
case message.RadioBand_RB_5G:
band = "5G"
}
wlaninfos := radioinfo.GetWlaninfo()
if wlaninfos == nil {
continue
}
for _, wlaninfo := range wlaninfos {
ssid := wlaninfo.GetSsid()
stas := wlaninfo.GetStas()
if stas == nil {
continue
}
for _, sta := range stas {
mac := sta.GetMac()
signal := sta.GetSignal()
rxrate := sta.GetRxrate()
txrate := sta.GetTxrate()
//ip, hostname := getinfofromdhcp(mac)
ip, hostname := grpcClient.GetStaInfo(mac)
response += fmt.Sprintf("%-15s %-17s %-11s %-4s %-17s %-10s %-17s %-11s %-4ddBm %-8s %-8s\n",
ip, mac, hostname, band, sn, apname, apmac, ssid, signal, rxrate, txrate)
}
}
}
return response
}

100
src/internal/telnet/tcp.go Executable file
View File

@@ -0,0 +1,100 @@
package telnet
import (
"fmt"
"net"
"strings"
"time"
)
var listener *net.TCPListener
var clientNum int
// Server - Init TCP Server
func Run(addrStr string) {
addr := fmt.Sprintf("[%s]:4100", addrStr)
tcpAddr, err := net.ResolveTCPAddr("tcp", addr)
if err != nil {
fmt.Println("[Telnet] net.ResolveTCPAddr fail:", addr)
return
}
listener, err := net.ListenTCP("tcp", tcpAddr)
if err != nil {
fmt.Println("[Telnet] failed to listen:", err)
return
}
fmt.Println("[Telnet] Listen on", addr)
go func() {
for {
conn, err := listener.Accept()
if err != nil {
fmt.Println(err.Error())
continue
} else if clientNum >= 9 {
fmt.Println("too many telnet request connection")
conn.Close()
continue
}
clientNum++
fmt.Println("[Telnet] A new Connection", clientNum)
fmt.Println("[Telnet] TCP Accept from:", conn.RemoteAddr().String())
go start(conn)
}
}()
}
// Start - Start TCP read channel
func start(conn net.Conn) {
defer closeConnection(conn)
conn.Write([]byte(help_menu))
conn.Write([]byte(prompt))
for {
buffer := make([]byte, 128)
conn.SetReadDeadline(time.Now().Add(time.Minute*10))
_, err := conn.Read(buffer)
if err != nil {
fmt.Println("[Telnet] Error", err)
break
}
buf := string(buffer)
pos := strings.Index(buf, "\n")
if pos < 0 || pos > 127 {
break
}
cmd := buf[:pos]
sep := strings.Fields(cmd)
if len(sep) <= 0 {
conn.Write([]byte(prompt))
continue
}
if sep[0] == "help" {
conn.Write([]byte(help_menu))
conn.Write([]byte(prompt))
continue
}
if sep[0] == "q" || sep[0] == "quit" {
break
}
rsp := HandleTelnetRequest(sep)
conn.Write([]byte(rsp))
conn.Write([]byte(prompt))
}
}
func closeConnection(conn net.Conn) {
conn.Close()
clientNum--
fmt.Printf("[Telnet] Now, %d connections is alive.\n", clientNum)
}
func Stop() {
if listener != nil {
listener.Close()
}
}

View File

@@ -0,0 +1,44 @@
package version
import (
"fmt"
"runtime"
)
var (
VERSION string
COMMIT_HASH string
COMMIT_TIME string
)
func GetVersion() string {
return VERSION
}
func GetVersionAndHash() string {
return VERSION + " (" + COMMIT_HASH + ")"
}
func GetVersionInfo() string {
if VERSION != "" {
return fmt.Sprintf(
"AC version: %s"+
"\ncommit hash: %s"+
"\ncommit time: %s"+
"\ngo version: %s %s/%s",
VERSION,
COMMIT_HASH,
COMMIT_TIME,
runtime.Version(),
runtime.GOOS,
runtime.GOARCH,
)
} else {
return fmt.Sprintf(
"Not specify ldflags (which link version) during go build\ngo version: %s %s/%s",
runtime.Version(),
runtime.GOOS,
runtime.GOARCH,
)
}
}

16
src/pkg/app/app.go Normal file
View File

@@ -0,0 +1,16 @@
package app
import (
ac_context "ac/internal/context"
"ac/pkg/factory"
)
type App interface {
SetLogLevel(level string)
Start()
Terminate()
Context() *ac_context.ACContext
Config() *factory.Config
}

71
src/pkg/factory/config.go Normal file
View File

@@ -0,0 +1,71 @@
/*
* AC Configuration Factory
*/
package factory
import (
"fmt"
"sync"
"github.com/asaskevich/govalidator"
"github.com/davecgh/go-spew/spew"
"ac/internal/logger"
)
const (
AcDefaultConfigPath = "/usr/local/etc/ac/ac.yaml"
CapwapDefaultPort = 5246
MqttDefaultPort = 5247
)
type Config struct {
DebugLevel string `yaml:"debugLevel" valid:"required,in(trace|debug|info|warn|error|fatal|panic)"`
CapwapAddr string `yaml:"capwapAddr,omitempty" valid:"required,host"`
sync.RWMutex
}
func (c *Config) Validate() (bool, error) {
if _, err := govalidator.ValidateStruct(c); err != nil {
return false, appendInvalid(err)
}
return true, nil
}
func appendInvalid(err error) error {
var errs govalidator.Errors
if err == nil {
return nil
}
es := err.(govalidator.Errors).Errors()
for _, e := range es {
errs = append(errs, fmt.Errorf("Invalid %w", e))
}
return error(errs)
}
func (c *Config) Print() {
spew.Config.Indent = "\t"
str := spew.Sdump(c)
logger.CfgLog.Infof("==================================================")
logger.CfgLog.Infof("%s", str)
logger.CfgLog.Infof("==================================================")
}
func (c *Config) SetLogLevel(level string) {
c.Lock()
defer c.Unlock()
c.DebugLevel = level
}
func (c *Config) GetLogLevel() string {
c.RLock()
defer c.RUnlock()
return c.DebugLevel
}

View File

@@ -0,0 +1,48 @@
/*
* AC Configuration Factory
*/
package factory
import (
"fmt"
"os"
"github.com/asaskevich/govalidator"
"gopkg.in/yaml.v2"
"ac/internal/logger"
)
var AcConfig *Config
// TODO: Support configuration update from REST api
func InitConfigFactory(cfg *Config) error {
if content, err := os.ReadFile(AcDefaultConfigPath); err != nil {
return fmt.Errorf("[Factory] %+v", err)
} else {
logger.CfgLog.Infof("Read config from [%s]", AcDefaultConfigPath)
if yamlErr := yaml.Unmarshal(content, cfg); yamlErr != nil {
return fmt.Errorf("[Factory] %+v", yamlErr)
}
}
return nil
}
func ReadConfig() (*Config, error) {
cfg := &Config{}
if err := InitConfigFactory(cfg); err != nil {
return nil, fmt.Errorf("ReadConfig [%s] Error: %+v", AcDefaultConfigPath, err)
}
if _, err := cfg.Validate(); err != nil {
validErrs := err.(govalidator.Errors).Errors()
for _, validErr := range validErrs {
logger.CfgLog.Errorf("%+v", validErr)
}
logger.CfgLog.Errorf("[-- PLEASE REFER TO SAMPLE CONFIG FILE COMMENTS --]")
return nil, fmt.Errorf("Config validate Error")
}
return cfg, nil
}

129
src/pkg/service/init.go Normal file
View File

@@ -0,0 +1,129 @@
package service
import (
"context"
"runtime/debug"
"sync"
"github.com/sirupsen/logrus"
"ac/internal/capwap"
capwap_service "ac/internal/capwap/service"
ac_context "ac/internal/context"
grpcClient "ac/internal/grpc_client"
"ac/internal/logger"
"ac/internal/mqtt"
"ac/internal/telnet"
"ac/pkg/app"
"ac/pkg/factory"
)
var AC app.App
type AcApp struct {
app.App
cfg *factory.Config
acCtx *ac_context.ACContext
ctx context.Context
cancel context.CancelFunc
wg sync.WaitGroup
mqttServer *mqtt.Server
}
func NewApp(ctx context.Context, cfg *factory.Config) (*AcApp, error) {
ac := &AcApp{
cfg: cfg,
}
ac.SetLogLevel(cfg.GetLogLevel())
ac.ctx, ac.cancel = context.WithCancel(ctx)
ac.acCtx = ac_context.GetSelf()
var err error
if ac.mqttServer, err = mqtt.NewServer(ac); err != nil {
return nil, err
}
AC = ac
return ac, nil
}
func (a *AcApp) SetLogLevel(level string) {
lvl, err := logrus.ParseLevel(level)
if err != nil {
logger.MainLog.Warnf("Log level [%s] is invalid", level)
return
}
logger.MainLog.Infof("Log level is set to [%s]", level)
if lvl == logger.Log.GetLevel() {
return
}
a.cfg.SetLogLevel(level)
logger.Log.SetLevel(lvl)
}
func (a *AcApp) Start() {
self := a.Context()
ac_context.InitAcContext(self)
capwap_service.Run(self.CapwapAddr, capwap.HandleMessage)
logger.InitLog.Infoln("Server started")
a.wg.Add(1)
go a.listenShutdownEvent()
telnet.Run("127.0.0.1")
grpcClient.ConnectToServer("127.0.0.1", 50051)
if err := a.mqttServer.Run(); err != nil {
logger.MainLog.Fatalf("Run MQTT server failed: %+v", err)
}
a.WaitRoutineStopped()
}
// Used in AC planned removal procedure
func (a *AcApp) Terminate() {
a.cancel()
}
func (a *AcApp) Config() *factory.Config {
return a.cfg
}
func (a *AcApp) Context() *ac_context.ACContext {
return a.acCtx
}
func (a *AcApp) CancelContext() context.Context {
return a.ctx
}
func (a *AcApp) listenShutdownEvent() {
defer func() {
if p := recover(); p != nil {
// Print stack for panic to log. Fatalf() will let program exit.
logger.MainLog.Fatalf("panic: %v\n%s", p, string(debug.Stack()))
}
a.wg.Done()
}()
<-a.ctx.Done()
a.terminateProcedure()
}
func (a *AcApp) WaitRoutineStopped() {
a.wg.Wait()
logger.MainLog.Infof("AC App is terminated")
}
func (a *AcApp) terminateProcedure() {
logger.MainLog.Infof("Terminating AC...")
capwap_service.Stop()
}