From 0242dad7e9ccb6d2b918ddf785b48d85ef5f427f Mon Sep 17 00:00:00 2001 From: andig Date: Wed, 8 Nov 2023 08:28:08 +0100 Subject: [PATCH] Modbus proxy: make readonly ignore writes, add deny option (BC) (#10658) --- cmd/root.go | 8 ++- cmd/setup.go | 2 +- evcc.dist.yaml | 2 +- server/modbus/handler.go | 16 ++++- server/modbus/proxy.go | 2 +- server/modbus/readonlymode.go | 11 ++++ server/modbus/readonlymode_enumer.go | 90 ++++++++++++++++++++++++++++ 7 files changed, 124 insertions(+), 7 deletions(-) create mode 100644 server/modbus/readonlymode.go create mode 100644 server/modbus/readonlymode_enumer.go diff --git a/cmd/root.go b/cmd/root.go index 65d9ee187..3afe61956 100644 --- a/cmd/root.go +++ b/cmd/root.go @@ -179,7 +179,13 @@ func runRoot(cmd *cobra.Command, args []string) { // setup modbus proxy if err == nil { for _, cfg := range conf.ModbusProxy { - if err = modbus.StartProxy(cfg.Port, cfg.Settings, cfg.ReadOnly); err != nil { + var mode modbus.ReadOnlyMode + mode, err = modbus.ReadOnlyModeString(cfg.ReadOnly) + if err != nil { + break + } + + if err = modbus.StartProxy(cfg.Port, cfg.Settings, mode); err != nil { break } } diff --git a/cmd/setup.go b/cmd/setup.go index 134b43444..3e218ab2b 100644 --- a/cmd/setup.go +++ b/cmd/setup.go @@ -113,7 +113,7 @@ type goConfig struct { type proxyConfig struct { Port int - ReadOnly bool + ReadOnly string modbus.Settings `mapstructure:",squash"` } diff --git a/evcc.dist.yaml b/evcc.dist.yaml index 5ed71c872..6ecbef10a 100644 --- a/evcc.dist.yaml +++ b/evcc.dist.yaml @@ -49,7 +49,7 @@ modbusproxy: # - port: 5200 # uri: solar-edge:502 # # rtu: true - # # readonly: true + # # readonly: true # use `deny` to raise modbus errors # meter definitions # name can be freely chosen and is used as reference when assigning meters to site and loadpoints diff --git a/server/modbus/handler.go b/server/modbus/handler.go index 035ef9fab..caae01aa9 100644 --- a/server/modbus/handler.go +++ b/server/modbus/handler.go @@ -13,7 +13,7 @@ import ( type handler struct { log *util.Logger - readOnly bool + readOnly ReadOnlyMode conn *modbus.Connection } @@ -104,8 +104,13 @@ func (h *handler) HandleDiscreteInputs(req *mbserver.DiscreteInputsRequest) ([]b func (h *handler) HandleCoils(req *mbserver.CoilsRequest) ([]bool, error) { if req.IsWrite { - if h.readOnly { + switch h.readOnly { + case ReadOnlyDeny: + h.log.TRACE.Printf("deny: write coils: id %d addr %d qty %d val %v", req.UnitId, req.Addr, req.Quantity, req.Args) return nil, mbserver.ErrIllegalFunction + case ReadOnlyTrue: + h.log.TRACE.Printf("ignore: write coils: id %d addr %d qty %d val %v", req.UnitId, req.Addr, req.Quantity, req.Args) + return req.Args, nil } if req.WriteFuncCode == gridx.FuncCodeWriteSingleCoil { @@ -138,8 +143,13 @@ func (h *handler) HandleInputRegisters(req *mbserver.InputRegistersRequest) ([]u func (h *handler) HandleHoldingRegisters(req *mbserver.HoldingRegistersRequest) ([]uint16, error) { if req.IsWrite { - if h.readOnly { + switch h.readOnly { + case ReadOnlyDeny: + h.log.TRACE.Printf("deny: write holdings: id %d addr %d qty %d val %0x", req.UnitId, req.Addr, req.Quantity, asBytes(req.Args)) return nil, mbserver.ErrIllegalFunction + case ReadOnlyTrue: + h.log.TRACE.Printf("ignore: write holdings: id %d addr %d qty %d val %0x", req.UnitId, req.Addr, req.Quantity, asBytes(req.Args)) + return req.Args, nil } if req.WriteFuncCode == gridx.FuncCodeWriteSingleRegister { diff --git a/server/modbus/proxy.go b/server/modbus/proxy.go index dc964bad5..096b6d7ac 100644 --- a/server/modbus/proxy.go +++ b/server/modbus/proxy.go @@ -11,7 +11,7 @@ import ( "github.com/evcc-io/evcc/util/sponsor" ) -func StartProxy(port int, config modbus.Settings, readOnly bool) error { +func StartProxy(port int, config modbus.Settings, readOnly ReadOnlyMode) error { conn, err := modbus.NewConnection(config.URI, config.Device, config.Comset, config.Baudrate, modbus.ProtocolFromRTU(config.RTU), config.ID) if err != nil { return err diff --git a/server/modbus/readonlymode.go b/server/modbus/readonlymode.go new file mode 100644 index 000000000..c7b5d7324 --- /dev/null +++ b/server/modbus/readonlymode.go @@ -0,0 +1,11 @@ +package modbus + +// go:generate enumer -type ReadOnlyMode -trimprefix ReadOnly -transform=lower + +type ReadOnlyMode int + +const ( + ReadOnlyFalse ReadOnlyMode = iota + ReadOnlyDeny + ReadOnlyTrue +) diff --git a/server/modbus/readonlymode_enumer.go b/server/modbus/readonlymode_enumer.go new file mode 100644 index 000000000..f19ea5ed7 --- /dev/null +++ b/server/modbus/readonlymode_enumer.go @@ -0,0 +1,90 @@ +// Code generated by "enumer -type ReadOnlyMode -trimprefix ReadOnly -transform=lower"; DO NOT EDIT. + +package modbus + +import ( + "fmt" + "strings" +) + +const _ReadOnlyModeName = "falsedenytrue" + +var _ReadOnlyModeIndex = [...]uint8{0, 5, 9, 13} + +const _ReadOnlyModeLowerName = "falsedenytrue" + +func (i ReadOnlyMode) String() string { + if i < 0 || i >= ReadOnlyMode(len(_ReadOnlyModeIndex)-1) { + return fmt.Sprintf("ReadOnlyMode(%d)", i) + } + return _ReadOnlyModeName[_ReadOnlyModeIndex[i]:_ReadOnlyModeIndex[i+1]] +} + +// An "invalid array index" compiler error signifies that the constant values have changed. +// Re-run the stringer command to generate them again. +func _ReadOnlyModeNoOp() { + var x [1]struct{} + _ = x[ReadOnlyFalse-(0)] + _ = x[ReadOnlyDeny-(1)] + _ = x[ReadOnlyTrue-(2)] +} + +var _ReadOnlyModeValues = []ReadOnlyMode{ReadOnlyFalse, ReadOnlyDeny, ReadOnlyTrue} + +var _ReadOnlyModeNameToValueMap = map[string]ReadOnlyMode{ + _ReadOnlyModeName[0:5]: ReadOnlyFalse, + _ReadOnlyModeLowerName[0:5]: ReadOnlyFalse, + _ReadOnlyModeName[5:9]: ReadOnlyDeny, + _ReadOnlyModeLowerName[5:9]: ReadOnlyDeny, + _ReadOnlyModeName[9:13]: ReadOnlyTrue, + _ReadOnlyModeLowerName[9:13]: ReadOnlyTrue, +} + +var _ReadOnlyModeNames = []string{ + _ReadOnlyModeName[0:5], + _ReadOnlyModeName[5:9], + _ReadOnlyModeName[9:13], +} + +// ReadOnlyModeString retrieves an enum value from the enum constants string name. +// Throws an error if the param is not part of the enum. +func ReadOnlyModeString(s string) (ReadOnlyMode, error) { + if val, ok := _ReadOnlyModeNameToValueMap[s]; ok { + return val, nil + } + + if val, ok := _ReadOnlyModeNameToValueMap[strings.ToLower(s)]; ok { + return val, nil + } + + switch s { + case "", "0": + return ReadOnlyFalse, nil + case "1": + return ReadOnlyTrue, nil + } + + return 0, fmt.Errorf("%s does not belong to ReadOnlyMode values", s) +} + +// ReadOnlyModeValues returns all values of the enum +func ReadOnlyModeValues() []ReadOnlyMode { + return _ReadOnlyModeValues +} + +// ReadOnlyModeStrings returns a slice of all String values of the enum +func ReadOnlyModeStrings() []string { + strs := make([]string, len(_ReadOnlyModeNames)) + copy(strs, _ReadOnlyModeNames) + return strs +} + +// IsAReadOnlyMode returns "true" if the value is listed in the enum definition. "false" otherwise +func (i ReadOnlyMode) IsAReadOnlyMode() bool { + for _, v := range _ReadOnlyModeValues { + if i == v { + return true + } + } + return false +}