├── .vscode └── launch.json ├── LICENSE ├── README.md ├── cl └── calllimiter.go ├── data.json ├── global ├── enums.go ├── guid.go ├── utilities.go └── variables.go ├── go.mod ├── go.sum ├── prometheus └── metrics.go ├── sip ├── headers.go ├── init.go ├── loadbalancer.go ├── message.go ├── server.go └── startline.go ├── slb.go └── webserver └── webserver.go /.vscode/launch.json: -------------------------------------------------------------------------------- 1 | { 2 | // Use IntelliSense to learn about possible attributes. 3 | // Hover to view descriptions of existing attributes. 4 | // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 5 | "version": "0.2.0", 6 | "configurations": [ 7 | { 8 | "name": "Launch", 9 | "type": "go", 10 | "request": "launch", //"attach", 11 | "mode": "debug", 12 | "program": "${workspaceFolder}/slb.go", 13 | "env": { 14 | "server_ipv4": "192.168.1.2", 15 | "sip_udp_port": "5060", 16 | "http_port": "9080", 17 | "lbmode": "RoundRobin", 18 | "sip_udp_servers": "192.168.1.2:5070" 19 | } 20 | } 21 | ] 22 | } 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Apache License 2 | Version 2.0, January 2004 3 | http://www.apache.org/licenses/ 4 | 5 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 6 | 7 | 1. Definitions. 8 | 9 | "License" shall mean the terms and conditions for use, reproduction, 10 | and distribution as defined by Sections 1 through 9 of this document. 11 | 12 | "Licensor" shall mean the copyright owner or entity authorized by 13 | the copyright owner that is granting the License. 14 | 15 | "Legal Entity" shall mean the union of the acting entity and all 16 | other entities that control, are controlled by, or are under common 17 | control with that entity. For the purposes of this definition, 18 | "control" means (i) the power, direct or indirect, to cause the 19 | direction or management of such entity, whether by contract or 20 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 21 | outstanding shares, or (iii) beneficial ownership of such entity. 22 | 23 | "You" (or "Your") shall mean an individual or Legal Entity 24 | exercising permissions granted by this License. 25 | 26 | "Source" form shall mean the preferred form for making modifications, 27 | including but not limited to software source code, documentation 28 | source, and configuration files. 29 | 30 | "Object" form shall mean any form resulting from mechanical 31 | transformation or translation of a Source form, including but 32 | not limited to compiled object code, generated documentation, 33 | and conversions to other media types. 34 | 35 | "Work" shall mean the work of authorship, whether in Source or 36 | Object form, made available under the License, as indicated by a 37 | copyright notice that is included in or attached to the work 38 | (an example is provided in the Appendix below). 39 | 40 | "Derivative Works" shall mean any work, whether in Source or Object 41 | form, that is based on (or derived from) the Work and for which the 42 | editorial revisions, annotations, elaborations, or other modifications 43 | represent, as a whole, an original work of authorship. For the purposes 44 | of this License, Derivative Works shall not include works that remain 45 | separable from, or merely link (or bind by name) to the interfaces of, 46 | the Work and Derivative Works thereof. 47 | 48 | "Contribution" shall mean any work of authorship, including 49 | the original version of the Work and any modifications or additions 50 | to that Work or Derivative Works thereof, that is intentionally 51 | submitted to Licensor for inclusion in the Work by the copyright owner 52 | or by an individual or Legal Entity authorized to submit on behalf of 53 | the copyright owner. For the purposes of this definition, "submitted" 54 | means any form of electronic, verbal, or written communication sent 55 | to the Licensor or its representatives, including but not limited to 56 | communication on electronic mailing lists, source code control systems, 57 | and issue tracking systems that are managed by, or on behalf of, the 58 | Licensor for the purpose of discussing and improving the Work, but 59 | excluding communication that is conspicuously marked or otherwise 60 | designated in writing by the copyright owner as "Not a Contribution." 61 | 62 | "Contributor" shall mean Licensor and any individual or Legal Entity 63 | on behalf of whom a Contribution has been received by Licensor and 64 | subsequently incorporated within the Work. 65 | 66 | 2. Grant of Copyright License. Subject to the terms and conditions of 67 | this License, each Contributor hereby grants to You a perpetual, 68 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 69 | copyright license to reproduce, prepare Derivative Works of, 70 | publicly display, publicly perform, sublicense, and distribute the 71 | Work and such Derivative Works in Source or Object form. 72 | 73 | 3. Grant of Patent License. Subject to the terms and conditions of 74 | this License, each Contributor hereby grants to You a perpetual, 75 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 76 | (except as stated in this section) patent license to make, have made, 77 | use, offer to sell, sell, import, and otherwise transfer the Work, 78 | where such license applies only to those patent claims licensable 79 | by such Contributor that are necessarily infringed by their 80 | Contribution(s) alone or by combination of their Contribution(s) 81 | with the Work to which such Contribution(s) was submitted. If You 82 | institute patent litigation against any entity (including a 83 | cross-claim or counterclaim in a lawsuit) alleging that the Work 84 | or a Contribution incorporated within the Work constitutes direct 85 | or contributory patent infringement, then any patent licenses 86 | granted to You under this License for that Work shall terminate 87 | as of the date such litigation is filed. 88 | 89 | 4. Redistribution. You may reproduce and distribute copies of the 90 | Work or Derivative Works thereof in any medium, with or without 91 | modifications, and in Source or Object form, provided that You 92 | meet the following conditions: 93 | 94 | (a) You must give any other recipients of the Work or 95 | Derivative Works a copy of this License; and 96 | 97 | (b) You must cause any modified files to carry prominent notices 98 | stating that You changed the files; and 99 | 100 | (c) You must retain, in the Source form of any Derivative Works 101 | that You distribute, all copyright, patent, trademark, and 102 | attribution notices from the Source form of the Work, 103 | excluding those notices that do not pertain to any part of 104 | the Derivative Works; and 105 | 106 | (d) If the Work includes a "NOTICE" text file as part of its 107 | distribution, then any Derivative Works that You distribute must 108 | include a readable copy of the attribution notices contained 109 | within such NOTICE file, excluding those notices that do not 110 | pertain to any part of the Derivative Works, in at least one 111 | of the following places: within a NOTICE text file distributed 112 | as part of the Derivative Works; within the Source form or 113 | documentation, if provided along with the Derivative Works; or, 114 | within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents 116 | of the NOTICE file are for informational purposes only and 117 | do not modify the License. You may add Your own attribution 118 | notices within Derivative Works that You distribute, alongside 119 | or as an addendum to the NOTICE text from the Work, provided 120 | that such additional attribution notices cannot be construed 121 | as modifying the License. 122 | 123 | You may add Your own copyright statement to Your modifications and 124 | may provide additional or different license terms and conditions 125 | for use, reproduction, or distribution of Your modifications, or 126 | for any such Derivative Works as a whole, provided Your use, 127 | reproduction, and distribution of the Work otherwise complies with 128 | the conditions stated in this License. 129 | 130 | 5. Submission of Contributions. Unless You explicitly state otherwise, 131 | any Contribution intentionally submitted for inclusion in the Work 132 | by You to the Licensor shall be under the terms and conditions of 133 | this License, without any additional terms or conditions. 134 | Notwithstanding the above, nothing herein shall supersede or modify 135 | the terms of any separate license agreement you may have executed 136 | with Licensor regarding such Contributions. 137 | 138 | 6. Trademarks. This License does not grant permission to use the trade 139 | names, trademarks, service marks, or product names of the Licensor, 140 | except as required for reasonable and customary use in describing the 141 | origin of the Work and reproducing the content of the NOTICE file. 142 | 143 | 7. Disclaimer of Warranty. Unless required by applicable law or 144 | agreed to in writing, Licensor provides the Work (and each 145 | Contributor provides its Contributions) on an "AS IS" BASIS, 146 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 147 | implied, including, without limitation, any warranties or conditions 148 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 149 | PARTICULAR PURPOSE. You are solely responsible for determining the 150 | appropriateness of using or redistributing the Work and assume any 151 | risks associated with Your exercise of permissions under this License. 152 | 153 | 8. Limitation of Liability. In no event and under no legal theory, 154 | whether in tort (including negligence), contract, or otherwise, 155 | unless required by applicable law (such as deliberate and grossly 156 | negligent acts) or agreed to in writing, shall any Contributor be 157 | liable to You for damages, including any direct, indirect, special, 158 | incidental, or consequential damages of any character arising as a 159 | result of this License or out of the use or inability to use the 160 | Work (including but not limited to damages for loss of goodwill, 161 | work stoppage, computer failure or malfunction, or any and all 162 | other commercial damages or losses), even if such Contributor 163 | has been advised of the possibility of such damages. 164 | 165 | 9. Accepting Warranty or Additional Liability. While redistributing 166 | the Work or Derivative Works thereof, You may choose to offer, 167 | and charge a fee for, acceptance of support, warranty, indemnity, 168 | or other liability obligations and/or rights consistent with this 169 | License. However, in accepting such obligations, You may act only 170 | on Your own behalf and on Your sole responsibility, not on behalf 171 | of any other Contributor, and only if You agree to indemnify, 172 | defend, and hold each Contributor harmless for any liability 173 | incurred by, or claims asserted against, such Contributor by reason 174 | of your accepting any such warranty or additional liability. 175 | 176 | END OF TERMS AND CONDITIONS 177 | 178 | APPENDIX: How to apply the Apache License to your work. 179 | 180 | To apply the Apache License to your work, attach the following 181 | boilerplate notice, with the fields enclosed by brackets "[]" 182 | replaced with your own identifying information. (Don't include 183 | the brackets!) The text should be enclosed in the appropriate 184 | comment syntax for the file format. We also recommend that a 185 | file or class name and description of purpose be included on the 186 | same "printed page" as the copyright notice for easier 187 | identification within third-party archives. 188 | 189 | Copyright [Moatassem Talaat (eng.moatassem@gmail.com)] 190 | 191 | Licensed under the Apache License, Version 2.0 (the "License"); 192 | you may not use this file except in compliance with the License. 193 | You may obtain a copy of the License at 194 | 195 | http://www.apache.org/licenses/LICENSE-2.0 196 | 197 | Unless required by applicable law or agreed to in writing, software 198 | distributed under the License is distributed on an "AS IS" BASIS, 199 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 200 | See the License for the specific language governing permissions and 201 | limitations under the License. 202 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # SIP Load Balancer v1.0.0 2 | 3 | Very fast load balancer for _SIP UDP traffic_ 4 | 5 | ## Load Balancing Algorithms: Determines how incoming requests are distributed: 6 | 7 | 1. **RoundRobin**: Distributes requests sequentially. 8 | 2. **MostIdle**: Sends requests to the most idle server. 9 | 3. **LeastCost**: Sends requests to the server with the least cost. 10 | 4. **LeastHit**: Sends requests to the server with the least hits. 11 | 5. **Weighted**: Sends requests to servers based on their assigned weight. If S1:3, S2:2 >> Result: S1, S2, S1, S2, S1, ... 12 | 6. **Random**: Sends requests to servers in a random order. 13 | 14 | ## Features: 15 | 16 | - **Health Checks**: Regularly check the status of each server to ensure it's capable of handling requests. 17 | - **Session Persistence (Sticky Sessions)**: Ensures requests from the same client are always sent to the same server. 18 | - **Failover**: Ensures requests are rerouted to healthy servers if a server fails. 19 | - **Scalability**: Ability to handle increasing traffic by adding more servers. 20 | 21 | ## Configuration: 22 | 23 | _See existing [data.json](/data.json) to edit the configuration_ 24 | 25 | Any changes in the json file, SLB needs to be rebooted. In the future, that won't be necessary. 26 | 27 | ```json 28 | { 29 | "ipv4": "192.168.1.2", // Server's IPv4 address 30 | "sipUdpPort": 5060, // SIP UDP port 31 | "httpPort": 9080, // HTTP TCP port 32 | "loadbalancemode": "RoundRobin", // Load balancing algorithm (case sensitive) 33 | "maxCallAttemptsPerSecond": 10000, // CAPS/Throttling limit (0=Disabled, -1=Unlimited, n=Custom) 34 | "probingInterval": 15, // SIP server health check interval (in seconds) 35 | "timeoutTimerDuration": 32, // Dialogue timeout (in seconds) [Ex. Egress server times out] 36 | "clearTimerDuration": 5, // Dialogue cleanup interval (in seconds) 37 | "servers": [ 38 | { 39 | "ipv4": "192.168.1.2", 40 | "port": 5077, 41 | "description": "SR1", 42 | "weight": 3, // Used for Weighted algorithm 43 | "cost": 5 // Used for LeastCost algorithm 44 | }, 45 | { 46 | "ipv4": "192.168.1.2", 47 | "port": 5070, 48 | "description": "SR2", 49 | "weight": 2, 50 | "cost": 5 51 | } 52 | ] 53 | } 54 | ``` 55 | 56 | ## Existing API calls: 57 | 58 | - `GET /api/v1/stats` 59 | Get general stats of the server 60 | - `GET /api/v1/config` 61 | Get running server configuration 62 | - `GET /api/v1/cache` 63 | Get cached SIP sessions 64 | -------------------------------------------------------------------------------- /cl/calllimiter.go: -------------------------------------------------------------------------------- 1 | package cl 2 | 3 | import ( 4 | "fmt" 5 | "sync" 6 | "time" 7 | 8 | "siploadbalancer/prometheus" 9 | ) 10 | 11 | const ( 12 | reset = "\033[0m" 13 | red = "\033[31m" 14 | green = "\033[32m" 15 | yellow = "\033[33m" 16 | blue = "\033[34m" 17 | purple = "\033[35m" 18 | cyan = "\033[36m" 19 | white = "\033[37m" 20 | gray = "\033[90m" 21 | ) 22 | 23 | type CallLimiter struct { 24 | rate int // rate limiter 25 | ticker *time.Ticker // ticker for timing 26 | callCount int // current call count 27 | mu sync.Mutex // mutex for thread safety 28 | } 29 | 30 | func NewCallLimiter(rate int, pm *prometheus.Metrics, wg *sync.WaitGroup) *CallLimiter { 31 | cl := &CallLimiter{ 32 | rate: rate, 33 | ticker: time.NewTicker(time.Second), 34 | } 35 | wg.Add(1) 36 | go cl.resetCount(pm, wg) 37 | 38 | fmt.Printf("Call Limiter set: %s\n", getCLstate(rate)) 39 | return cl 40 | } 41 | 42 | func getCLstate(rate int) string { 43 | switch rate { 44 | case -1: 45 | return fmt.Sprintf("%s%d %s%s", yellow, rate, "(Unlimited CAPS)", reset) 46 | case 0: 47 | return fmt.Sprintf("%s%d %s%s", red, rate, "(Server Disabled)", reset) 48 | default: 49 | return fmt.Sprintf("%s%d %s%s", green, rate, "CAPS", reset) 50 | } 51 | } 52 | 53 | func (clmtr *CallLimiter) resetCount(pm *prometheus.Metrics, wg *sync.WaitGroup) { 54 | defer wg.Done() 55 | for range clmtr.ticker.C { 56 | clmtr.mu.Lock() 57 | pm.Caps.Set(float64(clmtr.callCount)) 58 | clmtr.callCount = 0 59 | clmtr.mu.Unlock() 60 | } 61 | } 62 | 63 | func (clmtr *CallLimiter) IsExceeded() bool { 64 | clmtr.mu.Lock() 65 | defer clmtr.mu.Unlock() 66 | if clmtr.rate == -1 || clmtr.callCount < clmtr.rate { // it is not <= because i didn't add yet 1 to callCount 67 | clmtr.callCount++ 68 | return false // Call can be attempted 69 | } 70 | return true // Rate limit exceeded 71 | } 72 | -------------------------------------------------------------------------------- /data.json: -------------------------------------------------------------------------------- 1 | { 2 | "ipv4": "192.168.1.2", 3 | "sipUdpPort": 5060, 4 | "httpPort": 9080, 5 | "loadbalancemode": "RoundRobin", 6 | "maxCallAttemptsPerSecond": 10000, 7 | "probingInterval": 15, 8 | "timeoutTimerDuration": 32, 9 | "clearTimerDuration": 5, 10 | "servers": [ 11 | { 12 | "ipv4": "192.168.1.2", 13 | "port": 5077, 14 | "description": "SR1", 15 | "weight": 3, 16 | "cost": 5 17 | }, 18 | { 19 | "ipv4": "192.168.1.2", 20 | "port": 5070, 21 | "description": "SR2", 22 | "weight": 2, 23 | "cost": 5 24 | } 25 | ] 26 | } -------------------------------------------------------------------------------- /global/enums.go: -------------------------------------------------------------------------------- 1 | package global 2 | 3 | type Method string 4 | 5 | const ( 6 | UNKNOWN Method = "N/A" 7 | INVITE Method = "INVITE" 8 | ReINVITE Method = "INVITE" 9 | REFER Method = "REFER" 10 | ACK Method = "ACK" 11 | CANCEL Method = "CANCEL" 12 | BYE Method = "BYE" 13 | OPTIONS Method = "OPTIONS" 14 | NOTIFY Method = "NOTIFY" 15 | UPDATE Method = "UPDATE" 16 | PRACK Method = "PRACK" 17 | INFO Method = "INFO" 18 | REGISTER Method = "REGISTER" 19 | SUBSCRIBE Method = "SUBSCRIBE" 20 | MESSAGE Method = "MESSAGE" 21 | PUBLISH Method = "PUBLISH" 22 | NEGOTIATE Method = "NEGOTIATE" 23 | ) 24 | 25 | func GetMethod(hdrnm string) Method { 26 | switch hdrnm { 27 | case "INVITE": //ReINVITE included 28 | return INVITE 29 | case "REFER": 30 | return REFER 31 | case "ACK": 32 | return ACK 33 | case "CANCEL": 34 | return CANCEL 35 | case "BYE": 36 | return BYE 37 | case "OPTIONS": 38 | return OPTIONS 39 | case "NOTIFY": 40 | return NOTIFY 41 | case "UPDATE": 42 | return UPDATE 43 | case "PRACK": 44 | return PRACK 45 | case "INFO": 46 | return INFO 47 | case "REGISTER": 48 | return REGISTER 49 | case "SUBSCRIBE": 50 | return SUBSCRIBE 51 | case "MESSAGE": 52 | return MESSAGE 53 | case "PUBLISH": 54 | return PUBLISH 55 | case "NEGOTIATE": 56 | return NEGOTIATE 57 | default: 58 | return UNKNOWN 59 | } 60 | } 61 | 62 | // ============================================================== 63 | type MessageType = string 64 | 65 | const ( 66 | REQUEST MessageType = "REQUEST" 67 | RESPONSE MessageType = "RESPONSE" 68 | INVALID MessageType = "INVALID" 69 | ) 70 | 71 | type FieldPattern int 72 | 73 | const ( 74 | RequestStartLinePattern FieldPattern = iota 75 | INVITERURI 76 | ResponseStartLinePattern 77 | ViaBranchPattern 78 | FullHeader 79 | ViaIPv4Socket 80 | IP6 81 | IP4 82 | URIFull 83 | URIParameters 84 | URIParameter 85 | ErrorStack 86 | Tag 87 | ) 88 | 89 | type Header = string 90 | 91 | const ( 92 | Call_ID Header = "Call-ID" 93 | Content_Length Header = "Content-Length" 94 | From Header = "From" 95 | To Header = "To" 96 | Via Header = "Via" 97 | Server Header = "Server" 98 | CSeq Header = "CSeq" 99 | Max_Forwards Header = "Max-Forwards" 100 | Contact Header = "Contact" 101 | User_Agent Header = "User-Agent" 102 | ) 103 | -------------------------------------------------------------------------------- /global/guid.go: -------------------------------------------------------------------------------- 1 | package global 2 | 3 | import "github.com/google/uuid" 4 | 5 | func NewUUID() *uuid.UUID { 6 | u, _ := uuid.NewV7() 7 | return &u 8 | } 9 | 10 | func GetCallID() string { 11 | uid := NewUUID() 12 | return uid.String() 13 | } 14 | 15 | func GetViaBranch() string { 16 | uid := NewUUID() 17 | return MagicCookie + uid.String()[24:] 18 | } 19 | 20 | func GetTagOrKey() string { 21 | uid := NewUUID() 22 | return uid.String()[24:] 23 | } 24 | -------------------------------------------------------------------------------- /global/utilities.go: -------------------------------------------------------------------------------- 1 | package global 2 | 3 | import ( 4 | "bytes" 5 | "log" 6 | "math/rand/v2" 7 | "net" 8 | "runtime" 9 | "slices" 10 | "strings" 11 | ) 12 | 13 | func AreUAddrsEqual(addr1, addr2 *net.UDPAddr) bool { 14 | if addr1 == nil || addr2 == nil { 15 | return addr1 == addr2 16 | } 17 | return addr1.IP.Equal(addr2.IP) && addr1.Port == addr2.Port && addr1.Zone == addr2.Zone 18 | } 19 | 20 | func Str2Int[T int | int8 | int16 | int32 | int64](s string) T { 21 | var out T 22 | if len(s) == 0 { 23 | return out 24 | } 25 | idx := 0 26 | isN := s[idx] == '-' 27 | if isN { 28 | idx++ 29 | } 30 | for i := idx; i < len(s); i++ { 31 | if s[i] < '0' || s[i] > '9' { 32 | return out 33 | } 34 | out = out*10 + T(s[i]-'0') 35 | } 36 | if isN { 37 | return -out 38 | } 39 | return out 40 | } 41 | 42 | func Str2uint[T uint | uint8 | uint16 | uint32 | uint64](s string) T { 43 | var out T 44 | if len(s) == 0 { 45 | return out 46 | } 47 | for i := range len(s) { 48 | out = out*10 + T(s[i]-'0') 49 | } 50 | return out 51 | } 52 | 53 | func Str2Uint[T uint | uint8 | uint16 | uint32 | uint64](s string) T { 54 | var out T 55 | if len(s) == 0 { 56 | return out 57 | } 58 | for i := range len(s) { 59 | if s[i] < '0' || s[i] > '9' { 60 | return out 61 | } 62 | out = out*10 + T(s[i]-'0') 63 | } 64 | return out 65 | } 66 | 67 | func Int2Str(val int) string { 68 | if val == 0 { 69 | return "0" 70 | } 71 | buf := make([]byte, 10) 72 | return int2str(buf, val) 73 | } 74 | 75 | func Uint16ToStr(val uint16) string { 76 | if val == 0 { 77 | return "0" 78 | } 79 | buf := make([]byte, 5) 80 | return uint2str(buf, val) 81 | } 82 | 83 | // Uint32ToStr converts a uint32 to its string representation. 84 | func Uint32ToStr(val uint32) string { 85 | if val == 0 { 86 | return "0" 87 | } 88 | buf := make([]byte, 10) 89 | return uint2str(buf, val) 90 | } 91 | 92 | // Uint64ToStr converts a uint64 to its string representation. 93 | func Uint64ToStr(val uint64) string { 94 | if val == 0 { 95 | return "0" 96 | } 97 | buf := make([]byte, 20) 98 | return uint2str(buf, val) 99 | } 100 | 101 | func uint2str[T uint16 | uint32 | uint64](buf []byte, val T) string { 102 | i := len(buf) 103 | for val >= 10 { 104 | i-- 105 | buf[i] = '0' + byte(val%10) 106 | val /= 10 107 | } 108 | i-- 109 | buf[i] = '0' + byte(val) 110 | 111 | return string(buf[i:]) 112 | } 113 | 114 | func int2str[T int | int8 | int16 | int32 | int64](buf []byte, val T) string { 115 | isNeg := val < 0 116 | if isNeg { 117 | val *= -1 118 | } 119 | i := len(buf) 120 | for val >= 10 { 121 | i-- 122 | buf[i] = '0' + byte(val%10) 123 | val /= 10 124 | } 125 | i-- 126 | buf[i] = '0' + byte(val) 127 | 128 | if isNeg { 129 | return "-" + string(buf[i:]) 130 | } 131 | return string(buf[i:]) 132 | } 133 | 134 | func GetNextIndex(pdu []byte, markstrng string) int { 135 | return bytes.Index(pdu, []byte(markstrng)) 136 | } 137 | 138 | func BuildSipUdpSocket(host, port string) (*net.UDPAddr, error) { 139 | if port == "" { 140 | return net.ResolveUDPAddr("udp", host+":5060") 141 | } 142 | return net.ResolveUDPAddr("udp", host+":"+port) 143 | } 144 | 145 | func BuildUdpSocket(udpskt string) (*net.UDPAddr, error) { 146 | return net.ResolveUDPAddr("udp", udpskt) 147 | } 148 | 149 | func LogCallStack(r any) { 150 | log.Printf("Panic Recovered! Error:\n%v", r) 151 | buf := make([]byte, 1024) 152 | n := runtime.Stack(buf, false) 153 | log.Printf("Stack trace:\n%s\n", buf[:n]) 154 | } 155 | 156 | func RandomNumMinMax(min int, max int) int { 157 | return rand.IntN(max-min+1) + min 158 | } 159 | 160 | func RandomNum(max int) int { 161 | return rand.IntN(max) 162 | } 163 | 164 | func ASCIIToLower(s string) string { 165 | var b strings.Builder 166 | b.Grow(len(s)) 167 | for i := range len(s) { 168 | c := s[i] 169 | if 'A' <= c && c <= 'Z' { 170 | c += byte(DeltaRune) 171 | } 172 | b.WriteByte(c) 173 | } 174 | return b.String() 175 | } 176 | 177 | func ASCIIToUpper(s string) string { 178 | var b strings.Builder 179 | b.Grow(len(s)) 180 | for i := range len(s) { 181 | c := s[i] 182 | if 'a' <= c && c <= 'z' { 183 | c -= byte(DeltaRune) 184 | } 185 | b.WriteByte(c) 186 | } 187 | return b.String() 188 | } 189 | 190 | func RMatch(s string, rgxfp FieldPattern, mtch *[]string) bool { 191 | if s == "" { 192 | return false 193 | } 194 | *mtch = DicFieldRegEx[rgxfp].FindStringSubmatch(s) 195 | return *mtch != nil 196 | } 197 | 198 | func (m Method) IsDialogueCreating() bool { 199 | switch m { 200 | case OPTIONS, INVITE, MESSAGE, REGISTER, SUBSCRIBE: 201 | return true 202 | } 203 | return false 204 | } 205 | 206 | // ===================================================== 207 | 208 | func Find[T any](items []T, predicate func(T) bool) T { 209 | var out T 210 | for _, item := range items { 211 | if predicate(item) { 212 | return item 213 | } 214 | } 215 | return out 216 | } 217 | 218 | func All[T any](items []T, predicate func(T) bool) bool { 219 | if len(items) == 0 { 220 | return false 221 | } 222 | for _, item := range items { 223 | if !predicate(item) { 224 | return false 225 | } 226 | } 227 | return true 228 | } 229 | 230 | func AddIfNew[T comparable](items []T, item T) ([]T, bool) { 231 | ok := slices.Contains(items, item) 232 | if !ok { 233 | items = append(items, item) 234 | } 235 | return items, ok 236 | } 237 | 238 | func IsProvisional(sc int) bool { 239 | return 100 <= sc && sc <= 199 240 | } 241 | 242 | func IsProvisional18x(sc int) bool { 243 | return 180 <= sc && sc <= 189 244 | } 245 | 246 | func Is18xOrPositive(sc int) bool { 247 | return (180 <= sc && sc <= 189) || (200 <= sc && sc <= 299) 248 | } 249 | 250 | func IsFinal(sc int) bool { 251 | return 200 <= sc && sc <= 699 252 | } 253 | 254 | func IsPositive(sc int) bool { 255 | return 200 <= sc && sc <= 299 256 | } 257 | 258 | func IsNegative(sc int) bool { 259 | return 300 <= sc && sc <= 699 260 | } 261 | 262 | func IsRedirection(sc int) bool { 263 | return 300 <= sc && sc <= 399 264 | } 265 | 266 | func IsNegativeClient(sc int) bool { 267 | return 400 <= sc && sc <= 499 268 | } 269 | 270 | func IsNegativeServer(sc int) bool { 271 | return 500 <= sc && sc <= 599 272 | } 273 | 274 | func IsNegativeGlobal(sc int) bool { 275 | return 600 <= sc && sc <= 699 276 | } 277 | -------------------------------------------------------------------------------- /global/variables.go: -------------------------------------------------------------------------------- 1 | package global 2 | 3 | import ( 4 | "regexp" 5 | "siploadbalancer/cl" 6 | "siploadbalancer/prometheus" 7 | "sync" 8 | ) 9 | 10 | const ( 11 | BUE string = "SipLoadBalancer/v1.0" 12 | DeltaRune rune = 'a' - 'A' 13 | MagicCookie string = "z9hG4bK" 14 | ViaHeader string = "Via" 15 | SipVersion string = "SIP/2.0" 16 | ProbingTimeout int = 3 17 | ) 18 | 19 | var ( 20 | CallLimiter *cl.CallLimiter 21 | Prometrics *prometheus.Metrics 22 | WtGrp sync.WaitGroup 23 | ) 24 | 25 | var ( 26 | DicFieldRegEx = map[FieldPattern]*regexp.Regexp{ 27 | RequestStartLinePattern: regexp.MustCompile(`(?i)^\s*([a-z]+)\s+((?:\w+):(?:(?:[^@]+)@)?(?:[^@]+))\s+(SIP/2\.0)$`), 28 | INVITERURI: regexp.MustCompile(`(?i)([a-z]+):(?:([\*\#\+]?[a-z0-9\.\-\(\)]+)((?:[,;](?:[\w\-]+=[^@=,;:]+|[\w\-]+))*)(:[^@]+)?@)?([^\*\#\+@,:;]+)(?::(\d+))?((?:[,;](?:[\w\-]+=[^@,;\?]+|[\w\-]+))*)(?:(\?[^?]+))*`), 29 | ResponseStartLinePattern: regexp.MustCompile(`(?i)^\s*(SIP/2\.0)\s+(\d{3})(?:\s+([^,;]+)([,;].+)?)?$`), 30 | ViaBranchPattern: regexp.MustCompile(`(?i);branch\s*=\s*([^;,]+)`), 31 | FullHeader: regexp.MustCompile(`(?i)^\s*([^:]+)\s*:\s*(.+)$`), 32 | ViaIPv4Socket: regexp.MustCompile(`(?i)\s*SIP/2\.0\/(\w+)\s+((?:\d{1,3}\.){3}\d{1,3})(:\d+)?\s*`), 33 | IP6: regexp.MustCompile(`(?i)((?:(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){6})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:::(?:(?:(?:[0-9a-f]{1,4})):){5})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:[0-9a-f]{1,4})))?::(?:(?:(?:[0-9a-f]{1,4})):){4})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,1}(?:(?:[0-9a-f]{1,4})))?::(?:(?:(?:[0-9a-f]{1,4})):){3})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,2}(?:(?:[0-9a-f]{1,4})))?::(?:(?:(?:[0-9a-f]{1,4})):){2})(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,3}(?:(?:[0-9a-f]{1,4})))?::(?:(?:[0-9a-f]{1,4})):)(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,4}(?:(?:[0-9a-f]{1,4})))?::)(?:(?:(?:(?:(?:[0-9a-f]{1,4})):(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9]))\.){3}(?:(?:25[0-5]|(?:[1-9]|1[0-9]|2[0-4])?[0-9])))))))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,5}(?:(?:[0-9a-f]{1,4})))?::)(?:(?:[0-9a-f]{1,4})))|(?:(?:(?:(?:(?:(?:[0-9a-f]{1,4})):){0,6}(?:(?:[0-9a-f]{1,4})))?::)))))\s*$`), 34 | IP4: regexp.MustCompile(`(?i)((?:(?:2(?:5[0-5]|[0-4]\d)|1?\d?\d)\.){3}(?:2(?:5[0-5]|[0-4]\d)|1?\d?\d))\s*$`), 35 | URIFull: regexp.MustCompile(`(?i)((?:sip|sips|tel):[^>]+)`), 36 | URIParameters: regexp.MustCompile(`(?i)^(?:[,;](?:[\w\-]+|[\w\-]+=[^@=,;]+))*$`), 37 | URIParameter: regexp.MustCompile(`(?i)^([^=]+)=([^=]+)$`), 38 | ErrorStack: regexp.MustCompile(`(?i)(\w+\.vb):line\s(\d+)`), 39 | Tag: regexp.MustCompile(`(?i);tag=([^;]+)`), 40 | } 41 | ) 42 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module siploadbalancer 2 | 3 | go 1.24.3 4 | 5 | require ( 6 | github.com/google/uuid v1.6.0 7 | github.com/prometheus/client_golang v1.22.0 8 | ) 9 | 10 | require ( 11 | github.com/beorn7/perks v1.0.1 // indirect 12 | github.com/cespare/xxhash/v2 v2.3.0 // indirect 13 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect 14 | github.com/prometheus/client_model v0.6.2 // indirect 15 | github.com/prometheus/common v0.63.0 // indirect 16 | github.com/prometheus/procfs v0.16.1 // indirect 17 | golang.org/x/sys v0.33.0 // indirect 18 | google.golang.org/protobuf v1.36.6 // indirect 19 | ) 20 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= 2 | github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= 3 | github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= 4 | github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= 5 | github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= 6 | github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= 7 | github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8= 8 | github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= 9 | github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= 10 | github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= 11 | github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo= 12 | github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ= 13 | github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= 14 | github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= 15 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= 16 | github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= 17 | github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= 18 | github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 19 | github.com/prometheus/client_golang v1.22.0 h1:rb93p9lokFEsctTys46VnV1kLCDpVZ0a/Y92Vm0Zc6Q= 20 | github.com/prometheus/client_golang v1.22.0/go.mod h1:R7ljNsLXhuQXYZYtw6GAE9AZg8Y7vEW5scdCXrWRXC0= 21 | github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= 22 | github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= 23 | github.com/prometheus/common v0.63.0 h1:YR/EIY1o3mEFP/kZCD7iDMnLPlGyuU2Gb3HIcXnA98k= 24 | github.com/prometheus/common v0.63.0/go.mod h1:VVFF/fBIoToEnWRVkYoXEkq3R3paCoxG9PXP74SnV18= 25 | github.com/prometheus/procfs v0.16.1 h1:hZ15bTNuirocR6u0JZ6BAHHmwS1p8B4P6MRqxtzMyRg= 26 | github.com/prometheus/procfs v0.16.1/go.mod h1:teAbpZRB1iIAJYREa1LsoWUXykVXA1KlTmWl8x/U+Is= 27 | github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= 28 | github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= 29 | golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw= 30 | golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= 31 | google.golang.org/protobuf v1.36.6 h1:z1NpPI8ku2WgiWnf+t9wTPsn6eP1L7ksHUlkfLvd9xY= 32 | google.golang.org/protobuf v1.36.6/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY= 33 | gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= 34 | gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 35 | -------------------------------------------------------------------------------- /prometheus/metrics.go: -------------------------------------------------------------------------------- 1 | package prometheus 2 | 3 | import ( 4 | "net/http" 5 | 6 | "github.com/prometheus/client_golang/prometheus" 7 | "github.com/prometheus/client_golang/prometheus/collectors" 8 | "github.com/prometheus/client_golang/prometheus/promhttp" 9 | ) 10 | 11 | type Metrics struct { 12 | Registry *prometheus.Registry 13 | ConSessions prometheus.Gauge 14 | Caps prometheus.Gauge 15 | } 16 | 17 | func NewMetrics() *Metrics { 18 | reg := prometheus.NewRegistry() 19 | reg.MustRegister(collectors.NewGoCollector()) 20 | reg.MustRegister(collectors.NewProcessCollector(collectors.ProcessCollectorOpts{})) 21 | 22 | caps := prometheus.NewGauge(prometheus.GaugeOpts{ 23 | Namespace: "LoadBalancer", 24 | Name: "CallAttemptPerSecond", 25 | Help: "Shows concurrent sessions active", 26 | }) 27 | reg.MustRegister(caps) 28 | 29 | concurrentSessions := prometheus.NewGauge(prometheus.GaugeOpts{ 30 | Namespace: "LoadBalancer", 31 | Name: "ConcurrentSessions", 32 | Help: "Shows concurrent sessions active", 33 | }) 34 | reg.MustRegister(concurrentSessions) 35 | 36 | metrics := &Metrics{ 37 | Registry: reg, 38 | ConSessions: concurrentSessions, 39 | Caps: caps, 40 | } 41 | 42 | return metrics 43 | } 44 | 45 | func (m *Metrics) Handler() http.Handler { 46 | return promhttp.HandlerFor(m.Registry, promhttp.HandlerOpts{}) 47 | } 48 | -------------------------------------------------------------------------------- /sip/headers.go: -------------------------------------------------------------------------------- 1 | package sip 2 | 3 | import ( 4 | "fmt" 5 | "net" 6 | "slices" 7 | "strings" 8 | 9 | . "siploadbalancer/global" 10 | ) 11 | 12 | type SipHeaders struct { 13 | hmap map[string][]string 14 | hnames []string 15 | } 16 | 17 | func NewSipHeaders() *SipHeaders { 18 | headers := SipHeaders{hmap: make(map[string][]string)} 19 | return &headers 20 | } 21 | 22 | func (hdrs *SipHeaders) DecrementMaxForwards() bool { 23 | idx := hdrs.GetHeaderIndex(Max_Forwards) 24 | if idx == -1 { 25 | fmt.Printf("Could not find header [%s] values to amend", Max_Forwards) 26 | return false 27 | } 28 | 29 | mf := Str2Int[int](hdrs.hmap[hdrs.hnames[idx]][0]) 30 | mf-- 31 | 32 | if mf <= 0 { 33 | return false 34 | } 35 | 36 | hdrs.hmap[hdrs.hnames[idx]] = []string{Int2Str(mf)} 37 | 38 | return true 39 | } 40 | 41 | func (hdrs *SipHeaders) DropTopVia() { 42 | hdrs.DropTopHeaderValue(ViaHeader) 43 | } 44 | 45 | func buildViaHeader(viaBranch string) string { 46 | udpsocket := ServerConnection.LocalAddr().(*net.UDPAddr) 47 | return fmt.Sprintf("SIP/2.0/UDP %s;branch=%s", udpsocket, viaBranch) 48 | } 49 | 50 | func (hdrs *SipHeaders) AddTopVia(viaBranch string) { 51 | hdrs.AddTopHeaderValue(ViaHeader, buildViaHeader(viaBranch)) 52 | } 53 | 54 | func (hdrs *SipHeaders) Add(headerName string, headerValues ...string) { 55 | idx := hdrs.GetHeaderIndex(headerName) 56 | var hnm string 57 | if idx == -1 { 58 | hnm = headerName 59 | hdrs.hnames = append(hdrs.hnames, headerName) 60 | } else { 61 | hnm = hdrs.hnames[idx] 62 | } 63 | hdrs.hmap[hnm] = append(hdrs.hmap[hnm], headerValues...) 64 | } 65 | 66 | func (hdrs *SipHeaders) AddTopHeaderValue(headerName string, topValue string) { 67 | idx := hdrs.GetHeaderIndex(headerName) 68 | if idx == -1 { 69 | fmt.Printf("Could not find header [%s] values to amend", headerName) 70 | return 71 | } 72 | 73 | currentVia := hdrs.GetHeaderValues(ViaHeader) 74 | 75 | hvalues := make([]string, 0, 1+len(currentVia)) 76 | hvalues = append(hvalues, topValue) 77 | hvalues = append(hvalues, currentVia...) 78 | 79 | hdrs.hmap[hdrs.hnames[idx]] = hvalues 80 | } 81 | 82 | func (hdrs *SipHeaders) DropTopHeaderValue(headerName string) { 83 | idx := hdrs.GetHeaderIndex(headerName) 84 | if idx == -1 { 85 | fmt.Printf("Could not find header [%s] values to amend", headerName) 86 | return 87 | } 88 | 89 | currentVia := hdrs.hmap[ViaHeader] 90 | 91 | if len(currentVia) < 2 { 92 | fmt.Printf("Could not find enough header [%s] values to adjust", headerName) 93 | return 94 | } 95 | 96 | hdrs.hmap[hdrs.hnames[idx]] = currentVia[1:] 97 | } 98 | 99 | func (headers *SipHeaders) Values(headerName string) (bool, []string) { 100 | v, ok := headers.hmap[headerName] 101 | if ok { 102 | return true, v 103 | } 104 | 105 | return false, nil 106 | } 107 | 108 | func (hdrs *SipHeaders) GetHeaderIndex(hn string) int { 109 | return slices.IndexFunc(hdrs.hnames, func(x string) bool { return strings.EqualFold(x, hn) }) 110 | } 111 | 112 | func (hdrs *SipHeaders) GetHeaderValues(hn string) []string { 113 | idx := hdrs.GetHeaderIndex(hn) 114 | if idx == -1 { 115 | return nil 116 | } 117 | return hdrs.hmap[hdrs.hnames[idx]] 118 | } 119 | -------------------------------------------------------------------------------- /sip/init.go: -------------------------------------------------------------------------------- 1 | package sip 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "log" 7 | "net" 8 | "runtime" 9 | . "siploadbalancer/global" 10 | "strings" 11 | "sync" 12 | ) 13 | 14 | var ( 15 | ServerConnection *net.UDPConn 16 | WorkerCount = runtime.NumCPU() 17 | QueueSize = 3500 18 | packetQueue = make(chan Packet, QueueSize) 19 | BufferPool = newSyncPool() 20 | ) 21 | 22 | const ( 23 | BufferSize int = 4096 24 | ) 25 | 26 | func newSyncPool() *sync.Pool { 27 | return &sync.Pool{ 28 | New: func() any { 29 | lst := make([]byte, BufferSize) 30 | return &lst 31 | }, 32 | } 33 | } 34 | 35 | type Packet struct { 36 | sourceAddr *net.UDPAddr 37 | buffer *[]byte 38 | bytesCount int 39 | } 40 | 41 | func startWorkers() { 42 | WtGrp.Add(WorkerCount) 43 | for range WorkerCount { 44 | go worker(packetQueue) 45 | } 46 | } 47 | 48 | func udpLoopWorkers() { 49 | WtGrp.Add(1) 50 | go func() { 51 | WtGrp.Done() 52 | for { 53 | buf := BufferPool.Get().(*[]byte) 54 | n, addr, err := ServerConnection.ReadFromUDP(*buf) 55 | if err != nil { 56 | fmt.Println(err) 57 | continue 58 | } 59 | packetQueue <- Packet{sourceAddr: addr, buffer: buf, bytesCount: n} 60 | } 61 | }() 62 | } 63 | 64 | func worker(queue <-chan Packet) { 65 | defer WtGrp.Done() 66 | for packet := range queue { 67 | processPacket(packet) 68 | } 69 | } 70 | 71 | func processPacket(packet Packet) { 72 | pdu := (*packet.buffer)[:packet.bytesCount] 73 | for len(pdu) > 0 { 74 | msg, pdutmp, err := parsePDU(pdu) 75 | if err != nil { 76 | fmt.Println("Bad PDU -", err) 77 | fmt.Println(string(pdu)) 78 | break 79 | } else if msg == nil { 80 | break 81 | } 82 | callHandler(msg, packet.sourceAddr) 83 | pdu = pdutmp 84 | } 85 | BufferPool.Put(packet.buffer) 86 | } 87 | 88 | func parsePDU(payload []byte) (*SipMessage, []byte, error) { 89 | defer func() { 90 | if r := recover(); r != nil { 91 | // check if pdu is rqst >> send 400 with Warning header indicating what was wrong or unable to parse 92 | // or discard rqst if totally wrong 93 | // if pdu is rsps >> discard 94 | // in any case, log this pdu by saving its hex stream and why it was wrong 95 | LogCallStack(r) 96 | } 97 | }() 98 | 99 | var msgType MessageType 100 | var startLine SipStartLine 101 | 102 | sipmsg := new(SipMessage) 103 | msgmap := NewSipHeaders() 104 | 105 | var _dblCrLfIdx, _bodyStartIdx, lnIdx, cntntLength, cntntLengthComputed int 106 | 107 | _dblCrLfIdxInt := GetNextIndex(payload, "\r\n\r\n") 108 | 109 | if _dblCrLfIdxInt == -1 { 110 | // empty sip message 111 | return nil, nil, nil 112 | } 113 | 114 | _dblCrLfIdx = _dblCrLfIdxInt 115 | 116 | msglines := strings.Split(string(payload[:_dblCrLfIdx]), "\r\n") 117 | 118 | lnIdx = 0 119 | var matches []string 120 | // start line parsing 121 | if RMatch(msglines[lnIdx], RequestStartLinePattern, &matches) { 122 | msgType = REQUEST 123 | startLine.StatusCode = 0 124 | startLine.Method = GetMethod(ASCIIToUpper(matches[1])) 125 | if startLine.Method == UNKNOWN { 126 | return sipmsg, nil, errors.New("invalid method for Request message") 127 | } 128 | startLine.RUri = matches[2] 129 | if startLine.Method == INVITE && RMatch(startLine.RUri, INVITERURI, &matches) { 130 | startLine.Host = matches[5] 131 | startLine.Port = matches[6] 132 | } 133 | } else { 134 | if RMatch(msglines[lnIdx], ResponseStartLinePattern, &matches) { 135 | msgType = RESPONSE 136 | code := Str2Int[int](matches[2]) 137 | if code < 100 || code > 699 { 138 | return nil, nil, errors.New("invalid code for Response message") 139 | } 140 | startLine.StatusCode = code 141 | startLine.ReasonPhrase = matches[3] 142 | } else { 143 | sipmsg.MsgType = INVALID 144 | return sipmsg, nil, errors.New("invalid message") 145 | } 146 | } 147 | sipmsg.MsgType = msgType 148 | sipmsg.StartLine = startLine 149 | 150 | lnIdx += 1 151 | 152 | // headers parsing 153 | 154 | for i := lnIdx; i < len(msglines) && msglines[i] != ""; i++ { 155 | matches := DicFieldRegEx[FullHeader].FindStringSubmatch(msglines[i]) 156 | if matches != nil { 157 | headername := matches[1] 158 | headervalue := matches[2] 159 | switch { 160 | case strings.EqualFold(headername, From): 161 | tag := DicFieldRegEx[Tag].FindStringSubmatch(headervalue) 162 | if tag != nil { 163 | sipmsg.FromTag = tag[1] 164 | } 165 | case strings.EqualFold(headername, To): 166 | tag := DicFieldRegEx[Tag].FindStringSubmatch(headervalue) 167 | if tag != nil && tag[1] != "" { 168 | sipmsg.ToTag = tag[1] 169 | if startLine.Method == INVITE { 170 | startLine.Method = ReINVITE 171 | } 172 | } 173 | case strings.EqualFold(headername, Content_Length): 174 | cntntLength = Str2Int[int](headervalue) 175 | case strings.EqualFold(headername, Call_ID): 176 | sipmsg.CallID = headervalue 177 | case strings.EqualFold(headername, Via): 178 | via := DicFieldRegEx[ViaBranchPattern].FindStringSubmatch(headervalue) 179 | if via != nil && sipmsg.ViaBranch == "" { 180 | sipmsg.ViaBranch = via[1] 181 | } 182 | } 183 | msgmap.Add(headername, headervalue) 184 | } 185 | } 186 | 187 | _bodyStartIdx = _dblCrLfIdx + 4 // CrLf x 2 188 | 189 | // automatic deducing of content-length 190 | cntntLengthComputed = len(payload) - _bodyStartIdx 191 | 192 | if cntntLengthComputed != cntntLength { 193 | log.Printf("Discrepancy encountered in Content-Length - computed [%d] vs received [%d]", cntntLengthComputed, cntntLength) 194 | } 195 | 196 | sipmsg.Headers = msgmap 197 | 198 | // body parsing 199 | if cntntLength == 0 { 200 | payload = payload[_bodyStartIdx:] 201 | return sipmsg, payload, nil 202 | } 203 | 204 | sipmsg.Body = payload[_bodyStartIdx : _bodyStartIdx+cntntLength] 205 | 206 | payload = payload[_bodyStartIdx+cntntLength:] 207 | 208 | return sipmsg, payload, nil 209 | } 210 | 211 | func callHandler(sipmsg *SipMessage, srcAddr *net.UDPAddr) { 212 | defer func() { 213 | if r := recover(); r != nil { 214 | LogCallStack(r) 215 | } 216 | }() 217 | 218 | cc, rmtAddr := LoadBalancer.AddOrGetCallCache(sipmsg, srcAddr) 219 | if cc == nil || rmtAddr == nil { 220 | return 221 | } 222 | 223 | _, err := ServerConnection.WriteTo(sipmsg.Bytes(), rmtAddr) 224 | if err != nil { 225 | log.Println("Failed to forward message - error:", err) 226 | } 227 | } 228 | -------------------------------------------------------------------------------- /sip/loadbalancer.go: -------------------------------------------------------------------------------- 1 | package sip 2 | 3 | import ( 4 | "cmp" 5 | "fmt" 6 | "log" 7 | "net" 8 | 9 | . "siploadbalancer/global" 10 | "slices" 11 | "strings" 12 | "sync" 13 | "time" 14 | ) 15 | 16 | var LoadBalancer *LoadBalancingNode 17 | 18 | type ( 19 | LoadBalancingNode struct { 20 | SipNodes []*SipNode `json:"sipNodes"` 21 | Distribution Distribution `json:"distribution"` 22 | ProbingInterval int `json:"probingInterval"` 23 | TimeoutTimerDuration int `json:"timeoutTimerDuration"` 24 | ClearTimerDuration int `json:"clearTimerDuration"` 25 | 26 | sipNodesMap map[string]*SipNode `json:"-"` 27 | SipNodesLB []string `json:"sipNodesLB"` 28 | nodeIdx int `json:"-"` 29 | 30 | hitResetTicker *time.Ticker `json:"-"` 31 | callsCache map[string]*CallCache `json:"-"` 32 | mu sync.RWMutex `json:"-"` 33 | } 34 | 35 | SipNode struct { 36 | UdpAddr *net.UDPAddr 37 | Description string 38 | Cost int 39 | Weight int 40 | accWeight int 41 | 42 | Key string 43 | Hits int 44 | LastHit time.Time 45 | isAlive bool 46 | 47 | mu sync.RWMutex 48 | } 49 | 50 | Status string 51 | Distribution string 52 | 53 | CallCache struct { 54 | SIPNode *SipNode 55 | OtherAddr *net.UDPAddr 56 | IsInbound bool 57 | CallID string 58 | FromTag string 59 | OwnViaBranch string 60 | CallStatus Status 61 | Messages []string 62 | IsProbing bool 63 | 64 | timeoutTmr *time.Timer 65 | clearTmr *time.Timer 66 | mu sync.RWMutex 67 | } 68 | ) 69 | 70 | const ( 71 | StatusProgressing Status = "Progressing" // received Dialogue-creating methods 72 | StatusRejected Status = "Rejected" // received 3xx-6xx 73 | StatusAnswered Status = "Answered" // received 2xx 74 | StatusCancelled Status = "Cancelled" // received CANCEL 75 | StatusTimedout Status = "Timedout" // received no responses in time 76 | 77 | DistribRoundRobin Distribution = "RoundRobin" 78 | DistribLeastHit Distribution = "LeastHit" 79 | DistribLeastCost Distribution = "LeastCost" 80 | DistribMostIdle Distribution = "MostIdle" 81 | DistribWeighted Distribution = "Weighted" 82 | DistribRandom Distribution = "Random" 83 | 84 | LongTimeFormat string = "Mon, 02 Jan 2006 15:04:05 GMT" 85 | JsonTimeFormat string = "2006-01-02T15:04:05Z" 86 | 87 | HitResetDuration = 1 * time.Hour 88 | TimeoutTimerDD = 32 * time.Second // DD = Default Duration 89 | ClearTimerDD = 10 * time.Second 90 | ) 91 | 92 | func NewLoadBalancer(inputData inputData) *LoadBalancingNode { 93 | sipnodes := make([]*SipNode, 0, len(inputData.Servers)) 94 | sipNodesMap := make(map[string]*SipNode, len(inputData.Servers)) 95 | for _, srvr := range inputData.Servers { 96 | sipIpv4 := net.ParseIP(srvr.Ipv4) 97 | if sipIpv4 == nil { 98 | fmt.Printf("SIP Server IPv4: %s - invalid", srvr.Ipv4) 99 | continue 100 | } 101 | 102 | sipprt := srvr.Port 103 | if sipprt == 0 { 104 | fmt.Printf("SIP Server Port: %d - invalid", srvr.Port) 105 | continue 106 | } 107 | 108 | udpAddr := &net.UDPAddr{IP: sipIpv4, Port: sipprt, Zone: ""} 109 | 110 | if slices.ContainsFunc(sipnodes, func(x *SipNode) bool { 111 | return AreUAddrsEqual(x.UdpAddr, udpAddr) || strings.EqualFold(x.Description, srvr.Description) 112 | }) { 113 | fmt.Println("Duplicate Server record - Skipped") 114 | continue 115 | } 116 | 117 | sn := &SipNode{ 118 | Key: GetTagOrKey(), 119 | UdpAddr: udpAddr, 120 | Description: srvr.Description, 121 | Cost: srvr.Cost, 122 | Weight: srvr.Weight, 123 | accWeight: srvr.Weight, 124 | isAlive: false, 125 | Hits: 0, 126 | } 127 | 128 | sipnodes = append(sipnodes, sn) 129 | sipNodesMap[sn.Key] = sn 130 | } 131 | 132 | lbn := &LoadBalancingNode{ 133 | SipNodes: sipnodes, 134 | Distribution: Distribution(inputData.LoadbalanceMode), 135 | ProbingInterval: inputData.ProbingInterval, 136 | TimeoutTimerDuration: inputData.TimeoutTimerDuration, 137 | ClearTimerDuration: inputData.ClearTimerDuration, 138 | 139 | sipNodesMap: sipNodesMap, 140 | SipNodesLB: computeSipNodesLB(sipnodes), 141 | callsCache: make(map[string]*CallCache), 142 | } 143 | 144 | lbn.hitResetTicker = time.NewTicker(HitResetDuration) 145 | go lbn.hitResetTickerHandler() 146 | 147 | return lbn 148 | } 149 | 150 | func createClearTimer(callID string) *time.Timer { 151 | duration := time.Duration(LoadBalancer.ClearTimerDuration) * time.Second 152 | return time.AfterFunc(duration, func() { LoadBalancer.DeleteCallCache(callID) }) 153 | } 154 | 155 | func computeSipNodesLB(snlst []*SipNode) []string { 156 | grandweight := 0 157 | for _, wh := range snlst { 158 | grandweight += wh.Weight 159 | } 160 | 161 | lblst := make([]string, grandweight) 162 | for gw := range grandweight { 163 | sn := snlst[0] 164 | 165 | for i := len(snlst) - 1; i > 0; i-- { 166 | if snlst[i].accWeight >= sn.accWeight { 167 | sn = snlst[i] 168 | } 169 | } 170 | 171 | sn.accWeight -= grandweight 172 | 173 | for _, wh := range snlst { 174 | weight := wh.Weight 175 | accWeight := wh.accWeight 176 | wh.accWeight = weight + accWeight 177 | } 178 | 179 | lblst[gw] = sn.Key 180 | } 181 | 182 | return lblst 183 | } 184 | 185 | func (lb *LoadBalancingNode) hitResetTickerHandler() { 186 | for range lb.hitResetTicker.C { 187 | lb.mu.Lock() 188 | for _, sn := range lb.SipNodes { 189 | sn.ResetHits() 190 | } 191 | lb.mu.Unlock() 192 | } 193 | } 194 | 195 | func (lb *LoadBalancingNode) CallsCacheCount() int { 196 | lb.mu.RLock() 197 | defer lb.mu.RUnlock() 198 | 199 | return len(lb.callsCache) 200 | } 201 | 202 | func (lb *LoadBalancingNode) CallsCache() map[string]*CallCache { 203 | lb.mu.RLock() 204 | defer lb.mu.RUnlock() 205 | 206 | return lb.callsCache 207 | } 208 | 209 | func (lb *LoadBalancingNode) GetNode() *SipNode { 210 | lb.mu.Lock() 211 | defer lb.mu.Unlock() 212 | 213 | var outNode *SipNode 214 | for len(lb.SipNodes) > 0 && outNode == nil { 215 | switch lb.Distribution { 216 | case DistribRoundRobin: 217 | nd := lb.SipNodes[lb.nodeIdx] 218 | lb.nodeIdx++ 219 | if lb.nodeIdx >= len(lb.SipNodes) { 220 | lb.nodeIdx = 0 221 | } 222 | outNode = nd 223 | case DistribLeastHit: 224 | slices.SortFunc(lb.SipNodes, func(a, b *SipNode) int { return cmp.Compare(a.Hits, b.Hits) }) 225 | outNode = lb.SipNodes[0] 226 | case DistribLeastCost: 227 | slices.SortFunc(lb.SipNodes, func(a, b *SipNode) int { return cmp.Compare(a.Cost, b.Cost) }) 228 | outNode = lb.SipNodes[0] 229 | case DistribMostIdle: 230 | slices.SortFunc(lb.SipNodes, func(a, b *SipNode) int { 231 | if a.LastHit.Before(b.LastHit) { 232 | return -1 233 | } 234 | if a.LastHit.After(b.LastHit) { 235 | return 1 236 | } 237 | return 0 238 | }) 239 | outNode = lb.SipNodes[0] 240 | case DistribWeighted: 241 | ndKey := lb.SipNodesLB[lb.nodeIdx] 242 | lb.nodeIdx++ 243 | if lb.nodeIdx >= len(lb.SipNodesLB) { 244 | lb.nodeIdx = 0 245 | } 246 | outNode = lb.sipNodesMap[ndKey] 247 | default: // DistribRandom 248 | outNode = lb.SipNodes[RandomNum(len(lb.SipNodes))] 249 | } 250 | 251 | if outNode.IsDead() { 252 | if All(lb.SipNodes, func(x *SipNode) bool { return x.IsDead() }) { 253 | return nil 254 | } 255 | outNode = nil 256 | } 257 | } 258 | 259 | return outNode 260 | } 261 | 262 | func (lb *LoadBalancingNode) DeleteCallCache(callID string) { 263 | lb.mu.Lock() 264 | defer lb.mu.Unlock() 265 | delete(lb.callsCache, callID) 266 | Prometrics.ConSessions.Dec() 267 | } 268 | 269 | func (lb *LoadBalancingNode) ProbeSipNodes() { 270 | lb.mu.Lock() 271 | defer lb.mu.Unlock() 272 | 273 | for _, sn := range lb.SipNodes { 274 | callid := GetCallID() 275 | viaBranch := GetViaBranch() 276 | frmTag := GetTagOrKey() 277 | localstr := ServerConnection.LocalAddr().String() 278 | remotestr := sn.UdpAddr.String() 279 | 280 | probemsg := BuildOptionsMessage(viaBranch, localstr, remotestr, callid, frmTag) 281 | 282 | cc := &CallCache{ 283 | SIPNode: sn, 284 | CallID: callid, 285 | FromTag: frmTag, 286 | OwnViaBranch: viaBranch, 287 | CallStatus: StatusProgressing, 288 | IsProbing: true, 289 | } 290 | cc.StartTimeoutTimer(false) 291 | 292 | lb.callsCache[callid] = cc 293 | Prometrics.ConSessions.Inc() 294 | 295 | sendMessage(probemsg, sn.UdpAddr) 296 | } 297 | } 298 | 299 | func (lb *LoadBalancingNode) AddOrGetCallCache(sipmsg *SipMessage, srcAddr *net.UDPAddr) (*CallCache, *net.UDPAddr) { 300 | lb.mu.RLock() 301 | cc, ok := lb.callsCache[sipmsg.CallID] 302 | lb.mu.RUnlock() 303 | 304 | if ok { 305 | cc.mu.Lock() 306 | 307 | if cc.IsProbing { 308 | defer cc.mu.Unlock() 309 | 310 | if cc.timeoutTmr.Stop() { 311 | cc.SIPNode.SetAlive(true) 312 | LoadBalancer.DeleteCallCache(cc.CallID) 313 | } 314 | 315 | return nil, nil 316 | } 317 | 318 | var duplicateMsg bool 319 | cc.Messages, duplicateMsg = AddIfNew(cc.Messages, sipmsg.String()) 320 | 321 | if sipmsg.IsResponse() { 322 | cc.timeoutTmr.Stop() 323 | sipmsg.Headers.DropTopVia() 324 | 325 | stsCode := sipmsg.StartLine.StatusCode 326 | switch { 327 | case IsProvisional(stsCode): 328 | cc.CallStatus = StatusProgressing 329 | cc.StartTimeoutTimer(true) 330 | case IsPositive(stsCode): 331 | cc.timeoutTmr.Stop() 332 | cc.CallStatus = StatusAnswered 333 | cc.clearTmr = createClearTimer(cc.CallID) 334 | case IsNegative(stsCode): 335 | cc.timeoutTmr.Stop() 336 | cc.CallStatus = StatusRejected 337 | cc.clearTmr = createClearTimer(cc.CallID) 338 | } 339 | } else { 340 | if cc.IsInbound && duplicateMsg && cc.SIPNode.IsDead() { // if sipnode dies in the middle 341 | defer cc.mu.Unlock() 342 | sendMessage(BuildResponseMessage(sipmsg, 503, "Server Unreachable"), srcAddr) 343 | return nil, nil 344 | } 345 | sipmsg.Headers.AddTopVia(cc.OwnViaBranch) 346 | } 347 | 348 | cc.mu.Unlock() 349 | 350 | if AreUAddrsEqual(cc.OtherAddr, srcAddr) { 351 | return cc, cc.SIPNode.UdpAddr 352 | } 353 | 354 | return cc, cc.OtherAddr 355 | } 356 | 357 | if sipmsg.IsResponse() || !sipmsg.GetMethod().IsDialogueCreating() { 358 | // log.Printf("Message [%s] cannot initiate a dialogue - Dropping", sipmsg.String()) 359 | return nil, nil 360 | } 361 | 362 | if !sipmsg.Headers.DecrementMaxForwards() { 363 | sendMessage(BuildResponseMessage(sipmsg, 483, "Too Many Hops"), srcAddr) 364 | return nil, nil 365 | } 366 | 367 | var rmtAddr, azrAddr *net.UDPAddr 368 | var isingress bool 369 | 370 | sn := Find(lb.SipNodes, func(x *SipNode) bool { return AreUAddrsEqual(x.UdpAddr, srcAddr) }) 371 | if sn == nil { // inbound from Access to Core 372 | if CallLimiter.IsExceeded() { 373 | sendMessage(BuildResponseMessage(sipmsg, 480, "Call Limiter Exceeded"), srcAddr) 374 | return nil, nil 375 | } 376 | sn = lb.GetNode() 377 | if sn == nil { 378 | log.Printf("No more alive servers!") 379 | sendMessage(BuildResponseMessage(sipmsg, 503, "No Available Servers"), srcAddr) 380 | return nil, nil 381 | } 382 | sn.AddHit() 383 | azrAddr = srcAddr 384 | rmtAddr = sn.UdpAddr 385 | isingress = true 386 | } else { // outbound from Core to Access 387 | msgTargetAddr, err := BuildSipUdpSocket(sipmsg.StartLine.Host, sipmsg.StartLine.Port) 388 | if err != nil { 389 | log.Printf("Message [%s] contains unreachable host - Error [%s] - Dropping", sipmsg.String(), err) 390 | return nil, nil 391 | } 392 | azrAddr = msgTargetAddr 393 | rmtAddr = msgTargetAddr 394 | } 395 | 396 | cc = &CallCache{ 397 | SIPNode: sn, 398 | OtherAddr: azrAddr, 399 | IsInbound: isingress, 400 | CallID: sipmsg.CallID, 401 | FromTag: sipmsg.FromTag, 402 | OwnViaBranch: GetViaBranch(), 403 | CallStatus: StatusProgressing, 404 | Messages: []string{sipmsg.String()}, 405 | } 406 | cc.StartTimeoutTimer(false) 407 | 408 | lb.mu.Lock() 409 | lb.callsCache[sipmsg.CallID] = cc 410 | Prometrics.ConSessions.Inc() 411 | lb.mu.Unlock() 412 | 413 | sipmsg.Headers.AddTopVia(cc.OwnViaBranch) 414 | 415 | return cc, rmtAddr 416 | } 417 | 418 | func (cc *CallCache) timeoutHandler() { 419 | cc.mu.Lock() 420 | defer cc.mu.Unlock() 421 | 422 | if cc.IsProbing { 423 | cc.SIPNode.SetAlive(false) 424 | LoadBalancer.DeleteCallCache(cc.CallID) 425 | return 426 | } 427 | 428 | cc.clearTmr = createClearTimer(cc.CallID) 429 | cc.CallStatus = StatusTimedout 430 | } 431 | 432 | func (cc *CallCache) StartTimeoutTimer(dblDuration bool) { 433 | if !dblDuration { 434 | cc.mu.Lock() 435 | defer cc.mu.Unlock() 436 | } 437 | 438 | var interval int 439 | if cc.IsProbing { 440 | interval = ProbingTimeout 441 | } else { 442 | interval = LoadBalancer.TimeoutTimerDuration 443 | } 444 | 445 | duration := time.Duration(interval) * time.Second 446 | if dblDuration { 447 | duration *= 2 448 | } 449 | cc.timeoutTmr = time.AfterFunc(duration, func() { cc.timeoutHandler() }) 450 | } 451 | 452 | func (sn *SipNode) String() string { 453 | return fmt.Sprintf("%s (%s)", sn.Description, sn.UdpAddr) 454 | } 455 | 456 | func (sn *SipNode) AddHit() { 457 | sn.mu.Lock() 458 | defer sn.mu.Unlock() 459 | 460 | sn.Hits++ // TODO: find a way to rest this count! 461 | sn.LastHit = time.Now().UTC() 462 | } 463 | 464 | func (sn *SipNode) ResetHits() { 465 | sn.mu.Lock() 466 | defer sn.mu.Unlock() 467 | 468 | sn.Hits = 0 469 | } 470 | 471 | func (sn *SipNode) SetAlive(flag bool) { 472 | sn.mu.Lock() 473 | defer sn.mu.Unlock() 474 | 475 | if sn.isAlive != flag { 476 | stamp := time.Now().UTC().Format(JsonTimeFormat) 477 | var newsts string 478 | if flag { 479 | newsts = "ALIVE" 480 | } else { 481 | newsts = "DEAD" 482 | } 483 | fmt.Printf("%s became %s on %s\n", sn, newsts, stamp) 484 | } 485 | 486 | sn.isAlive = flag 487 | } 488 | 489 | func (sn *SipNode) IsDead() bool { 490 | sn.mu.RLock() 491 | defer sn.mu.RUnlock() 492 | 493 | return !sn.isAlive 494 | } 495 | 496 | func sendMessage(sipmsg *SipMessage, rmtUDPAddr *net.UDPAddr) { 497 | _, err := ServerConnection.WriteTo(sipmsg.Bytes(), rmtUDPAddr) 498 | if err != nil { 499 | log.Println("Failed to send response message - error:", err) 500 | } 501 | } 502 | -------------------------------------------------------------------------------- /sip/message.go: -------------------------------------------------------------------------------- 1 | package sip 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | . "siploadbalancer/global" 7 | ) 8 | 9 | type SipMessage struct { 10 | MsgType MessageType 11 | StartLine SipStartLine 12 | Headers *SipHeaders 13 | Body []byte 14 | 15 | CallID string 16 | FromTag string 17 | ToTag string 18 | ViaBranch string 19 | } 20 | 21 | func BuildOptionsMessage(viaBranch, localstr, remotestr, callid, frmTag string) *SipMessage { 22 | hdrs := NewSipHeaders() 23 | hdrs.Add(Via, buildViaHeader(viaBranch)) 24 | hdrs.Add(From, fmt.Sprintf(";tag=%s", localstr, frmTag)) 25 | hdrs.Add(To, fmt.Sprintf("", remotestr)) 26 | hdrs.Add(Call_ID, callid) 27 | hdrs.Add(CSeq, fmt.Sprintf("911 %s", OPTIONS)) 28 | hdrs.Add(Contact, fmt.Sprintf("", localstr)) 29 | hdrs.Add(Max_Forwards, "70") 30 | hdrs.Add(User_Agent, BUE) 31 | hdrs.Add(Content_Length, "0") 32 | 33 | return &SipMessage{ 34 | MsgType: REQUEST, 35 | StartLine: SipStartLine{ 36 | Method: OPTIONS, 37 | RUri: fmt.Sprintf("sip:%s", remotestr), 38 | }, 39 | Headers: hdrs, 40 | } 41 | } 42 | 43 | func BuildResponseMessage(rqstmsg *SipMessage, sc int, rp string) *SipMessage { 44 | hdrs := NewSipHeaders() 45 | hdrs.Add(Via, rqstmsg.Headers.GetHeaderValues(ViaHeader)...) 46 | hdrs.Add(From, rqstmsg.Headers.GetHeaderValues(From)...) 47 | hdrs.Add(To, rqstmsg.Headers.GetHeaderValues(To)...) 48 | hdrs.Add(Call_ID, rqstmsg.CallID) 49 | hdrs.Add(CSeq, rqstmsg.Headers.GetHeaderValues(CSeq)...) 50 | hdrs.Add(Server, BUE) 51 | hdrs.Add(Content_Length, "0") 52 | 53 | rspnsmsg := &SipMessage{ 54 | MsgType: RESPONSE, 55 | StartLine: SipStartLine{ 56 | StatusCode: sc, 57 | ReasonPhrase: rp, 58 | }, 59 | Headers: hdrs, 60 | } 61 | 62 | return rspnsmsg 63 | } 64 | 65 | // ========================================================================== 66 | 67 | func (sipmsg *SipMessage) String() string { 68 | if sipmsg.MsgType == REQUEST { 69 | return string(sipmsg.StartLine.Method) 70 | } 71 | 72 | return Int2Str(sipmsg.StartLine.StatusCode) 73 | } 74 | 75 | func (sipmsg *SipMessage) Bytes() []byte { 76 | var bb bytes.Buffer 77 | 78 | // startline 79 | if sipmsg.IsRequest() { 80 | sl := sipmsg.StartLine 81 | bb.WriteString(sl.BuildStartLine(REQUEST)) 82 | } else { 83 | sl := sipmsg.StartLine 84 | bb.WriteString(sl.BuildStartLine(RESPONSE)) 85 | } 86 | 87 | // headers - build and write 88 | for _, h := range sipmsg.Headers.hnames { 89 | _, values := sipmsg.Headers.Values(h) 90 | for _, hv := range values { 91 | if hv != "" { 92 | bb.WriteString(fmt.Sprintf("%s: %s\r\n", h, hv)) 93 | } 94 | } 95 | } 96 | 97 | // write separator 98 | bb.WriteString("\r\n") 99 | 100 | // write body bytes 101 | bb.Write(sipmsg.Body) 102 | 103 | return bb.Bytes() 104 | } 105 | 106 | // =========================================================================== 107 | 108 | func (sipmsg *SipMessage) IsOutOfDialgoue() bool { 109 | return sipmsg.ToTag == "" 110 | } 111 | 112 | func (sipmsg *SipMessage) IsResponse() bool { 113 | return sipmsg.MsgType == RESPONSE 114 | } 115 | 116 | func (sipmsg *SipMessage) IsRequest() bool { 117 | return sipmsg.MsgType == REQUEST 118 | } 119 | 120 | func (sipmsg *SipMessage) GetMethod() Method { 121 | return sipmsg.StartLine.Method 122 | } 123 | 124 | func (sipmsg *SipMessage) GetStatusCode() int { 125 | return sipmsg.StartLine.StatusCode 126 | } 127 | 128 | // ====================================== 129 | -------------------------------------------------------------------------------- /sip/server.go: -------------------------------------------------------------------------------- 1 | package sip 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "net" 7 | "os" 8 | "siploadbalancer/global" 9 | "time" 10 | ) 11 | 12 | type inputData struct { 13 | IPv4 string `json:"ipv4"` 14 | SipUdpPort int `json:"sipUdpPort"` 15 | HttpPort int `json:"httpPort"` 16 | CachingServer string `json:"cachingServer"` 17 | 18 | LoadbalanceMode string `json:"loadbalancemode"` 19 | MaxCallAttemptsPerSecond int `json:"maxCallAttemptsPerSecond"` 20 | ProbingInterval int `json:"probingInterval"` 21 | TimeoutTimerDuration int `json:"timeoutTimerDuration"` 22 | ClearTimerDuration int `json:"clearTimerDuration"` 23 | 24 | Servers []struct { 25 | Ipv4 string `json:"ipv4"` 26 | Port int `json:"port"` 27 | Description string `json:"description"` 28 | Weight int `json:"weight"` 29 | Cost int `json:"cost"` 30 | } `json:"servers"` 31 | } 32 | 33 | func startListening(ip net.IP, prt int) (*net.UDPConn, error) { 34 | socket := net.UDPAddr{} 35 | socket.IP = ip 36 | socket.Port = prt 37 | return net.ListenUDP("udp", &socket) 38 | } 39 | 40 | func InitializeServer(data []byte) (net.IP, int, int) { 41 | var ( 42 | inputData inputData 43 | err error 44 | ) 45 | 46 | if err = json.Unmarshal(data, &inputData); err != nil { 47 | fmt.Println(err) 48 | os.Exit(1) 49 | } 50 | 51 | serverIP := net.ParseIP(inputData.IPv4) 52 | fmt.Print("Attempting to listen on SIP...") 53 | if ServerConnection, err = startListening(serverIP, inputData.SipUdpPort); err != nil { 54 | fmt.Println(err) 55 | os.Exit(2) 56 | } 57 | fmt.Println("Success: UDP", ServerConnection.LocalAddr().String()) 58 | 59 | fmt.Print("Checking Caching Server...") 60 | // ripv4skt, err := redis.SetupCheckRedis(redisskt, "", 0, 15) //TODO: add redis password, db and expiryMin 61 | // if err != nil { 62 | // fmt.Println(err) 63 | // os.Exit(3) 64 | // } 65 | // fmt.Printf("Ready! [%s]\n", ripv4skt) 66 | fmt.Println("Skipped!") 67 | 68 | LoadBalancer = NewLoadBalancer(inputData) 69 | 70 | return serverIP, inputData.HttpPort, inputData.MaxCallAttemptsPerSecond 71 | } 72 | 73 | func StartSS() { 74 | startWorkers() 75 | udpLoopWorkers() 76 | periodicProbing() 77 | 78 | fmt.Println("SipLoadBalancer Server Ready!") 79 | } 80 | 81 | func periodicProbing() { 82 | global.WtGrp.Add(1) 83 | duration := time.Duration(LoadBalancer.ProbingInterval) * time.Second 84 | ticker := time.NewTicker(duration) 85 | LoadBalancer.ProbeSipNodes() // to run once when system starts 86 | go func() { 87 | defer global.WtGrp.Done() 88 | for range ticker.C { 89 | LoadBalancer.ProbeSipNodes() 90 | } 91 | }() 92 | } 93 | -------------------------------------------------------------------------------- /sip/startline.go: -------------------------------------------------------------------------------- 1 | package sip 2 | 3 | import ( 4 | "fmt" 5 | . "siploadbalancer/global" 6 | ) 7 | 8 | type SipStartLine struct { 9 | Method Method 10 | Host string 11 | Port string 12 | 13 | RUri string 14 | 15 | StatusCode int 16 | ReasonPhrase string 17 | } 18 | 19 | func (ssl *SipStartLine) BuildStartLine(mt MessageType) string { 20 | if mt == REQUEST { 21 | return fmt.Sprintf("%s %s %s\r\n", ssl.Method, ssl.RUri, SipVersion) 22 | } 23 | return fmt.Sprintf("%s %d %s\r\n", SipVersion, ssl.StatusCode, ssl.ReasonPhrase) 24 | } 25 | -------------------------------------------------------------------------------- /slb.go: -------------------------------------------------------------------------------- 1 | /* 2 | # Software Name : SIPLoadBalancer 3 | 4 | # Author: 5 | # - Moatassem Talaat 6 | 7 | --- 8 | */ 9 | 10 | package main 11 | 12 | import ( 13 | "fmt" 14 | "os" 15 | "path/filepath" 16 | "siploadbalancer/cl" 17 | "siploadbalancer/global" 18 | "siploadbalancer/prometheus" 19 | "siploadbalancer/sip" 20 | "siploadbalancer/webserver" 21 | ) 22 | 23 | func greeting() { 24 | fmt.Printf("Welcome to MT %s\n", global.BUE) 25 | } 26 | 27 | func main() { 28 | greeting() 29 | global.Prometrics = prometheus.NewMetrics() 30 | ip, hp, rate := sip.InitializeServer(readJsonFile()) 31 | global.CallLimiter = cl.NewCallLimiter(rate, global.Prometrics, &global.WtGrp) 32 | // defer sip.ServerConnection.Close() 33 | webserver.StartWS(ip, hp) 34 | sip.StartSS() 35 | global.WtGrp.Wait() 36 | } 37 | 38 | func readJsonFile() []byte { 39 | exePath, err := os.Executable() 40 | if err != nil { 41 | fmt.Println("Error getting executable path:", err) 42 | os.Exit(1) 43 | } 44 | exeDir := filepath.Dir(exePath) 45 | 46 | jsonPath := filepath.Join(exeDir, "data.json") 47 | 48 | data, err := os.ReadFile(jsonPath) 49 | if err != nil { 50 | fmt.Println("Error reading JSON file:", err) 51 | os.Exit(1) 52 | } 53 | 54 | return data 55 | } 56 | -------------------------------------------------------------------------------- /webserver/webserver.go: -------------------------------------------------------------------------------- 1 | package webserver 2 | 3 | import ( 4 | "encoding/json" 5 | "fmt" 6 | "log" 7 | "net" 8 | "net/http" 9 | "runtime" 10 | . "siploadbalancer/global" 11 | "siploadbalancer/sip" 12 | ) 13 | 14 | func StartWS(ip net.IP, hp int) { 15 | r := http.NewServeMux() 16 | 17 | r.HandleFunc("GET /api/v1/stats", serveStats) 18 | r.HandleFunc("GET /api/v1/config", serveConfig) 19 | r.HandleFunc("GET /api/v1/cache", serveCache) 20 | r.Handle("GET /metrics", Prometrics.Handler()) 21 | r.HandleFunc("GET /", serveHome) 22 | 23 | ws := fmt.Sprintf("%s:%d", ip, hp) 24 | 25 | WtGrp.Add(1) 26 | go func() { 27 | defer WtGrp.Done() 28 | log.Fatal(http.ListenAndServe(ws, r)) 29 | }() 30 | 31 | log.Println("Loading API Webserver...") 32 | log.Printf("Success: HTTP %s\n", ws) 33 | 34 | log.Printf("Prometheus metrics available at http://%s/metrics\n", ws) 35 | } 36 | 37 | func serveHome(w http.ResponseWriter, r *http.Request) { 38 | _, err := w.Write(fmt.Appendf(nil, "

%s API Webserver

\n", BUE)) 39 | if err != nil { 40 | log.Println(err) 41 | } 42 | } 43 | 44 | func serveConfig(w http.ResponseWriter, r *http.Request) { 45 | w.Header().Set("Content-Type", "application/json") 46 | 47 | response, _ := json.Marshal(sip.LoadBalancer) 48 | _, err := w.Write(response) 49 | if err != nil { 50 | log.Println(err) 51 | } 52 | } 53 | 54 | func serveCache(w http.ResponseWriter, r *http.Request) { 55 | w.Header().Set("Content-Type", "application/json") 56 | response, _ := json.Marshal(sip.LoadBalancer.CallsCache()) 57 | _, err := w.Write(response) 58 | if err != nil { 59 | log.Println(err) 60 | } 61 | } 62 | 63 | func serveStats(w http.ResponseWriter, r *http.Request) { 64 | w.Header().Set("Content-Type", "application/json") 65 | 66 | var m runtime.MemStats 67 | runtime.ReadMemStats(&m) 68 | 69 | BToMB := func(b uint64) uint64 { 70 | return b / 1000 / 1000 71 | } 72 | 73 | data := struct { 74 | CPUCount int 75 | GoRoutinesCount int 76 | Alloc uint64 77 | System uint64 78 | GCCycles uint32 79 | CallsCacheCount int 80 | }{ 81 | CPUCount: runtime.NumCPU(), 82 | GoRoutinesCount: runtime.NumGoroutine(), 83 | Alloc: BToMB(m.Alloc), 84 | System: BToMB(m.Sys), 85 | GCCycles: m.NumGC, 86 | CallsCacheCount: sip.LoadBalancer.CallsCacheCount(), 87 | } 88 | 89 | response, _ := json.Marshal(data) 90 | _, err := w.Write(response) 91 | if err != nil { 92 | log.Println(err) 93 | } 94 | } 95 | --------------------------------------------------------------------------------