├── LICENSE ├── README.md ├── client └── config │ ├── config.go │ ├── config_test.go │ └── templates │ └── client-config.tpl └── server ├── config ├── config.go ├── config_test.go └── templates │ └── server-config.tpl └── mi ├── README.md ├── client.go ├── command.go ├── command_test.go ├── parse.go ├── parse_test.go └── types.go /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2017 Adam Wałach 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## go-openvpn 2 | 3 | Set of Golang libraries for OpenVPN 4 | 5 | List of libraries: 6 | * [client/config](client/config) - Helper library for generation of a OpenVPN server configuration file 7 | * [server/config](server/config) - Helper library for generation of a OpenVPN client configuration file 8 | * [serever/mi](server/mi) - OpenVPN Management Interface library 9 | 10 | ## todo: 11 | * easy rsa wrapper - something similar to https://github.com/reddec/vpn-control 12 | -------------------------------------------------------------------------------- /client/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "bytes" 5 | "html/template" 6 | "io/ioutil" 7 | ) 8 | 9 | var defaultConfig = Config{ 10 | ServerAddress: "127.0.0.1", 11 | Port: 1194, 12 | Proto: "udp", 13 | Cipher: "AES-256-CBC", 14 | Keysize: 256, 15 | Auth: "SHA256", 16 | Ca: "ca.crt", 17 | } 18 | 19 | //Config model 20 | type Config struct { 21 | ServerAddress string 22 | Port int 23 | Proto string 24 | 25 | Ca string 26 | Cert string 27 | Key string 28 | 29 | Cipher string 30 | Keysize int 31 | Auth string 32 | } 33 | 34 | //New returns config object with default values 35 | func New() Config { 36 | return defaultConfig 37 | } 38 | 39 | //GetText injects config values into template 40 | func GetText(tpl string, c Config) (string, error) { 41 | t := template.New("config") 42 | t, err := t.Parse(tpl) 43 | if err != nil { 44 | return "", err 45 | } 46 | buf := new(bytes.Buffer) 47 | t.Execute(buf, c) 48 | return buf.String(), nil 49 | } 50 | 51 | //SaveToFile reads teamplate and writes result to destination file 52 | func SaveToFile(tplPath string, c Config, destPath string) error { 53 | template, err := ioutil.ReadFile(tplPath) 54 | if err != nil { 55 | return err 56 | } 57 | 58 | str, err := GetText(string(template), c) 59 | if err != nil { 60 | return err 61 | } 62 | 63 | return ioutil.WriteFile(destPath, []byte(str), 0644) 64 | } 65 | -------------------------------------------------------------------------------- /client/config/config_test.go: -------------------------------------------------------------------------------- 1 | package config_test 2 | 3 | import ( 4 | "io/ioutil" 5 | "testing" 6 | 7 | "github.com/adamwalach/go-openvpn/client/config" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestNewConfig(t *testing.T) { 12 | c := config.New() 13 | assert.Equal(t, c.Auth, "SHA256") 14 | } 15 | 16 | func TestTemplateGeneration(t *testing.T) { 17 | c := config.New() 18 | txt, err := ioutil.ReadFile("./templates/client-config.tpl") 19 | assert.Nil(t, err) 20 | 21 | _, err = config.GetText(string(txt), c) 22 | assert.Nil(t, err) 23 | } 24 | 25 | func TestBrokenTemplate(t *testing.T) { 26 | c := config.New() 27 | 28 | _, err := config.GetText("{{ ", c) 29 | assert.NotNil(t, err, "Parser should fail on broken template") 30 | } 31 | -------------------------------------------------------------------------------- /client/config/templates/client-config.tpl: -------------------------------------------------------------------------------- 1 | dev tun 2 | persist-tun 3 | persist-key 4 | client 5 | resolv-retry infinite 6 | remote {{ .ServerAddress }} {{ .Port }} {{ .Proto }} 7 | lport 0 8 | 9 | cipher {{ .Cipher }} 10 | keysize {{ .Keysize }} 11 | auth {{ .Auth }} 12 | tls-client 13 | 14 | ca {{ .Ca }} 15 | cert {{ .Cert }} 16 | key {{ .Key }} 17 | 18 | comp-lzo 19 | -------------------------------------------------------------------------------- /server/config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | "bytes" 5 | "html/template" 6 | "io/ioutil" 7 | ) 8 | 9 | var defaultConfig = Config{ 10 | Port: 1194, 11 | Proto: "udp", 12 | Cipher: "AES-256-CBC", 13 | Keysize: 256, 14 | Auth: "SHA256", 15 | Dh: "dh2048.pem", 16 | Keepalive: "10 120", 17 | IfconfigPoolPersist: "ipp.txt", 18 | } 19 | 20 | //Config model 21 | type Config struct { 22 | Port int 23 | Proto string 24 | 25 | Ca string 26 | Cert string 27 | Key string 28 | 29 | Cipher string 30 | Keysize int 31 | Auth string 32 | Dh string 33 | 34 | Server string 35 | IfconfigPoolPersist string 36 | Keepalive string 37 | MaxClients int 38 | 39 | Management string 40 | } 41 | 42 | //New returns config object with default values 43 | func New() Config { 44 | return defaultConfig 45 | } 46 | 47 | //GetText injects config values into template 48 | func GetText(tpl string, c Config) (string, error) { 49 | t := template.New("config") 50 | t, err := t.Parse(tpl) 51 | if err != nil { 52 | return "", err 53 | } 54 | buf := new(bytes.Buffer) 55 | t.Execute(buf, c) 56 | return buf.String(), nil 57 | } 58 | 59 | //SaveToFile reads teamplate and writes result to destination file 60 | func SaveToFile(tplPath string, c Config, destPath string) error { 61 | template, err := ioutil.ReadFile(tplPath) 62 | if err != nil { 63 | return err 64 | } 65 | 66 | str, err := GetText(string(template), c) 67 | if err != nil { 68 | return err 69 | } 70 | 71 | return ioutil.WriteFile(destPath, []byte(str), 0644) 72 | } 73 | -------------------------------------------------------------------------------- /server/config/config_test.go: -------------------------------------------------------------------------------- 1 | package config_test 2 | 3 | import ( 4 | "io/ioutil" 5 | "testing" 6 | 7 | "github.com/adamwalach/go-openvpn/server/config" 8 | "github.com/stretchr/testify/assert" 9 | ) 10 | 11 | func TestNewConfig(t *testing.T) { 12 | c := config.New() 13 | assert.Equal(t, c.Auth, "SHA256") 14 | } 15 | 16 | func TestTemplateGeneration(t *testing.T) { 17 | c := config.New() 18 | txt, err := ioutil.ReadFile("./templates/server-config.tpl") 19 | assert.Nil(t, err) 20 | 21 | _, err = config.GetText(string(txt), c) 22 | assert.Nil(t, err) 23 | } 24 | 25 | func TestBrokenTemplate(t *testing.T) { 26 | c := config.New() 27 | 28 | _, err := config.GetText("{{ ", c) 29 | assert.NotNil(t, err, "Parser should fail on broken template") 30 | } 31 | -------------------------------------------------------------------------------- /server/config/templates/server-config.tpl: -------------------------------------------------------------------------------- 1 | ################################################# 2 | # OpenVPN 2.0 config file for # 3 | # multi-client server. # 4 | # # 5 | # This file is for the server side # 6 | # of a many-clients one-server # 7 | # OpenVPN configuration. # 8 | # # 9 | # OpenVPN also supports # 10 | # single-machine single-machine # 11 | # configurations (See the Examples page # 12 | # on the web site for more info). # 13 | # # 14 | # This config should work on Windows # 15 | # or Linux/BSD systems. Remember on # 16 | # Windows to quote pathnames and use # 17 | # double backslashes, e.g.: # 18 | # "C:\\Program Files\\OpenVPN\\config\\foo.key" # 19 | # # 20 | # Comments are preceded with '#' or ';' # 21 | ################################################# 22 | 23 | management {{ .Management }} 24 | 25 | # Which local IP address should OpenVPN 26 | # listen on? (optional) 27 | ;local a.b.c.d 28 | 29 | # Which TCP/UDP port should OpenVPN listen on? 30 | # If you want to run multiple OpenVPN instances 31 | # on the same machine, use a different port 32 | # number for each one. You will need to 33 | # open up this port on your firewall. 34 | port {{ .Port }} 35 | 36 | # TCP or UDP server? 37 | proto {{ .Proto }} 38 | 39 | # "dev tun" will create a routed IP tunnel, 40 | # "dev tap" will create an ethernet tunnel. 41 | # Use "dev tap0" if you are ethernet bridging 42 | # and have precreated a tap0 virtual interface 43 | # and bridged it with your ethernet interface. 44 | # If you want to control access policies 45 | # over the VPN, you must create firewall 46 | # rules for the the TUN/TAP interface. 47 | # On non-Windows systems, you can give 48 | # an explicit unit number, such as tun0. 49 | # On Windows, use "dev-node" for this. 50 | # On most systems, the VPN will not function 51 | # unless you partially or fully disable 52 | # the firewall for the TUN/TAP interface. 53 | dev tun 54 | 55 | # Windows needs the TAP-Win32 adapter name 56 | # from the Network Connections panel if you 57 | # have more than one. On XP SP2 or higher, 58 | # you may need to selectively disable the 59 | # Windows firewall for the TAP adapter. 60 | # Non-Windows systems usually don't need this. 61 | ;dev-node MyTap 62 | 63 | # SSL/TLS root certificate (ca), certificate 64 | # (cert), and private key (key). Each client 65 | # and the server must have their own cert and 66 | # key file. The server and all clients will 67 | # use the same ca file. 68 | # 69 | # See the "easy-rsa" directory for a series 70 | # of scripts for generating RSA certificates 71 | # and private keys. Remember to use 72 | # a unique Common Name for the server 73 | # and each of the client certificates. 74 | # 75 | # Any X509 key management system can be used. 76 | # OpenVPN can also use a PKCS #12 formatted key file 77 | # (see "pkcs12" directive in man page). 78 | ca ca.crt 79 | cert server.crt 80 | key server.key # This file should be kept secret 81 | 82 | cipher {{ .Cipher }} 83 | keysize {{ .Keysize }} 84 | auth {{ .Auth }} 85 | 86 | # Diffie hellman parameters. 87 | # Generate your own with: 88 | # openssl dhparam -out dh1024.pem 1024 89 | # Substitute 2048 for 1024 if you are using 90 | # 2048 bit keys. 91 | dh dh2048.pem 92 | 93 | # Configure server mode and supply a VPN subnet 94 | # for OpenVPN to draw client addresses from. 95 | # The server will take 10.8.0.1 for itself, 96 | # the rest will be made available to clients. 97 | # Each client will be able to reach the server 98 | # on 10.8.0.1. Comment this line out if you are 99 | # ethernet bridging. See the man page for more info. 100 | server 10.8.0.0 255.255.255.0 101 | 102 | # Maintain a record of client virtual IP address 103 | # associations in this file. If OpenVPN goes down or 104 | # is restarted, reconnecting clients can be assigned 105 | # the same virtual IP address from the pool that was 106 | # previously assigned. 107 | ifconfig-pool-persist {{ .IfconfigPoolPersist }} 108 | 109 | # Configure server mode for ethernet bridging. 110 | # You must first use your OS's bridging capability 111 | # to bridge the TAP interface with the ethernet 112 | # NIC interface. Then you must manually set the 113 | # IP/netmask on the bridge interface, here we 114 | # assume 10.8.0.4/255.255.255.0. Finally we 115 | # must set aside an IP range in this subnet 116 | # (start=10.8.0.50 end=10.8.0.100) to allocate 117 | # to connecting clients. Leave this line commented 118 | # out unless you are ethernet bridging. 119 | ;server-bridge 10.8.0.4 255.255.255.0 10.8.0.50 10.8.0.100 120 | 121 | # Configure server mode for ethernet bridging 122 | # using a DHCP-proxy, where clients talk 123 | # to the OpenVPN server-side DHCP server 124 | # to receive their IP address allocation 125 | # and DNS server addresses. You must first use 126 | # your OS's bridging capability to bridge the TAP 127 | # interface with the ethernet NIC interface. 128 | # Note: this mode only works on clients (such as 129 | # Windows), where the client-side TAP adapter is 130 | # bound to a DHCP client. 131 | ;server-bridge 132 | 133 | # Push routes to the client to allow it 134 | # to reach other private subnets behind 135 | # the server. Remember that these 136 | # private subnets will also need 137 | # to know to route the OpenVPN client 138 | # address pool (10.8.0.0/255.255.255.0) 139 | # back to the OpenVPN server. 140 | ;push "route 192.168.10.0 255.255.255.0" 141 | ;push "route 192.168.20.0 255.255.255.0" 142 | push "route 10.8.0.0 255.255.255.0" 143 | 144 | # To assign specific IP addresses to specific 145 | # clients or if a connecting client has a private 146 | # subnet behind it that should also have VPN access, 147 | # use the subdirectory "ccd" for client-specific 148 | # configuration files (see man page for more info). 149 | 150 | # EXAMPLE: Suppose the client 151 | # having the certificate common name "Thelonious" 152 | # also has a small subnet behind his connecting 153 | # machine, such as 192.168.40.128/255.255.255.248. 154 | # First, uncomment out these lines: 155 | ;client-config-dir ccd 156 | ;route 192.168.40.128 255.255.255.248 157 | # Then create a file ccd/Thelonious with this line: 158 | # iroute 192.168.40.128 255.255.255.248 159 | # This will allow Thelonious' private subnet to 160 | # access the VPN. This example will only work 161 | # if you are routing, not bridging, i.e. you are 162 | # using "dev tun" and "server" directives. 163 | 164 | # EXAMPLE: Suppose you want to give 165 | # Thelonious a fixed VPN IP address of 10.9.0.1. 166 | # First uncomment out these lines: 167 | ;client-config-dir ccd 168 | ;route 10.9.0.0 255.255.255.252 169 | # Then add this line to ccd/Thelonious: 170 | # ifconfig-push 10.9.0.1 10.9.0.2 171 | 172 | # Suppose that you want to enable different 173 | # firewall access policies for different groups 174 | # of clients. There are two methods: 175 | # (1) Run multiple OpenVPN daemons, one for each 176 | # group, and firewall the TUN/TAP interface 177 | # for each group/daemon appropriately. 178 | # (2) (Advanced) Create a script to dynamically 179 | # modify the firewall in response to access 180 | # from different clients. See man 181 | # page for more info on learn-address script. 182 | ;learn-address ./script 183 | 184 | # If enabled, this directive will configure 185 | # all clients to redirect their default 186 | # network gateway through the VPN, causing 187 | # all IP traffic such as web browsing and 188 | # and DNS lookups to go through the VPN 189 | # (The OpenVPN server machine may need to NAT 190 | # or bridge the TUN/TAP interface to the internet 191 | # in order for this to work properly). 192 | # push "redirect-gateway def1 bypass-dhcp" 193 | 194 | # Certain Windows-specific network settings 195 | # can be pushed to clients, such as DNS 196 | # or WINS server addresses. CAVEAT: 197 | # http://openvpn.net/faq.html#dhcpcaveats 198 | # The addresses below refer to the public 199 | # DNS servers provided by opendns.com. 200 | #push "dhcp-option DNS 208.67.222.222" 201 | #push "dhcp-option DNS 208.67.220.220" 202 | push "dhcp-option DNS 8.8.8.8" 203 | push "dhcp-option DNS 8.8.4.4" 204 | 205 | # Uncomment this directive to allow different 206 | # clients to be able to "see" each other. 207 | # By default, clients will only see the server. 208 | # To force clients to only see the server, you 209 | # will also need to appropriately firewall the 210 | # server's TUN/TAP interface. 211 | ;client-to-client 212 | 213 | # Uncomment this directive if multiple clients 214 | # might connect with the same certificate/key 215 | # files or common names. This is recommended 216 | # only for testing purposes. For production use, 217 | # each client should have its own certificate/key 218 | # pair. 219 | # 220 | # IF YOU HAVE NOT GENERATED INDIVIDUAL 221 | # CERTIFICATE/KEY PAIRS FOR EACH CLIENT, 222 | # EACH HAVING ITS OWN UNIQUE "COMMON NAME", 223 | # UNCOMMENT THIS LINE OUT. 224 | ;duplicate-cn 225 | 226 | # The keepalive directive causes ping-like 227 | # messages to be sent back and forth over 228 | # the link so that each side knows when 229 | # the other side has gone down. 230 | # Ping every 10 seconds, assume that remote 231 | # peer is down if no ping received during 232 | # a 120 second time period. 233 | keepalive {{ .Keepalive }} 234 | 235 | # For extra security beyond that provided 236 | # by SSL/TLS, create an "HMAC firewall" 237 | # to help block DoS attacks and UDP port flooding. 238 | # 239 | # Generate with: 240 | # openvpn --genkey --secret ta.key 241 | # 242 | # The server and each client must have 243 | # a copy of this key. 244 | # The second parameter should be '0' 245 | # on the server and '1' on the clients. 246 | ;tls-auth ta.key 0 # This file is secret 247 | 248 | # Select a cryptographic cipher. 249 | # This config item must be copied to 250 | # the client config file as well. 251 | ;cipher BF-CBC # Blowfish (default) 252 | ;cipher AES-128-CBC # AES 253 | ;cipher DES-EDE3-CBC # Triple-DES 254 | 255 | # Enable compression on the VPN link. 256 | # If you enable it here, you must also 257 | # enable it in the client config file. 258 | comp-lzo 259 | 260 | # The maximum number of concurrently connected 261 | # clients we want to allow. 262 | max-clients {{ .MaxClients }} 263 | 264 | # It's a good idea to reduce the OpenVPN 265 | # daemon's privileges after initialization. 266 | # 267 | # You can uncomment this out on 268 | # non-Windows systems. 269 | ;user nobody 270 | ;group nogroup 271 | 272 | # The persist options will try to avoid 273 | # accessing certain resources on restart 274 | # that may no longer be accessible because 275 | # of the privilege downgrade. 276 | persist-key 277 | persist-tun 278 | 279 | # Output a short status file showing 280 | # current connections, truncated 281 | # and rewritten every minute. 282 | status openvpn-status.log 283 | 284 | # By default, log messages will go to the syslog (or 285 | # on Windows, if running as a service, they will go to 286 | # the "\Program Files\OpenVPN\log" directory). 287 | # Use log or log-append to override this default. 288 | # "log" will truncate the log file on OpenVPN startup, 289 | # while "log-append" will append to it. Use one 290 | # or the other (but not both). 291 | log openvpn.log 292 | ;log-append openvpn.log 293 | 294 | # Set the appropriate level of log 295 | # file verbosity. 296 | # 297 | # 0 is silent, except for fatal errors 298 | # 4 is reasonable for general usage 299 | # 5 and 6 can help to debug connection problems 300 | # 9 is extremely verbose 301 | verb 3 302 | 303 | # Silence repeating messages. At most 20 304 | # sequential messages of the same message 305 | # category will be output to the log. 306 | ;mute 20 -------------------------------------------------------------------------------- /server/mi/README.md: -------------------------------------------------------------------------------- 1 | 2 | Library for interacting with [OpenVPN Management Interface](https://openvpn.net/index.php/open-source/documentation/miscellaneous/79-management-interface.html) 3 | -------------------------------------------------------------------------------- /server/mi/client.go: -------------------------------------------------------------------------------- 1 | package mi 2 | 3 | import ( 4 | "bufio" 5 | "net" 6 | ) 7 | 8 | //Client is used to connect to OpenVPN Management Interface 9 | type Client struct { 10 | MINetwork string 11 | MIAddress string 12 | } 13 | 14 | //NewClient initializes Management Interface client structure 15 | func NewClient(network, address string) *Client { 16 | c := &Client{ 17 | MINetwork: network, //Management Interface Network 18 | MIAddress: address, //Management Interface Address 19 | } 20 | 21 | return c 22 | } 23 | 24 | //GetPid returns process id of OpenVPN server 25 | func (c *Client) GetPid() (int64, error) { 26 | str, err := c.Execute("pid") 27 | if err != nil { 28 | return -1, err 29 | } 30 | return ParsePid(str) 31 | } 32 | 33 | //GetVersion returns version of OpenVPN server 34 | func (c *Client) GetVersion() (*Version, error) { 35 | str, err := c.Execute("version") 36 | if err != nil { 37 | return nil, err 38 | } 39 | return ParseVersion(str) 40 | } 41 | 42 | //GetStatus returns list of connected clients and routing table 43 | func (c *Client) GetStatus() (*Status, error) { 44 | str, err := c.Execute("status 2") 45 | if err != nil { 46 | return nil, err 47 | } 48 | return ParseStatus(str) 49 | } 50 | 51 | //GetLoadStats returns number of connected clients and total number of network traffic 52 | func (c *Client) GetLoadStats() (*LoadStats, error) { 53 | str, err := c.Execute("load-stats") 54 | if err != nil { 55 | return nil, err 56 | } 57 | return ParseStats(str) 58 | } 59 | 60 | //KillSession kills OpenVPN connection 61 | func (c *Client) KillSession(cname string) (string, error) { 62 | str, err := c.Execute("kill " + cname) 63 | if err != nil { 64 | return "", err 65 | } 66 | return ParseKillSession(str) 67 | } 68 | 69 | //Signal sends signal to daemon 70 | func (c *Client) Signal(signal string) error { 71 | str, err := c.Execute("signal " + signal) 72 | if err != nil { 73 | return err 74 | } 75 | return ParseSignal(str) 76 | } 77 | 78 | //Execute connects to the OpenVPN server, sends command and reads response 79 | func (c *Client) Execute(cmd string) (string, error) { 80 | conn, err := net.Dial(c.MINetwork, c.MIAddress) 81 | if err != nil { 82 | return "", err 83 | } 84 | defer conn.Close() 85 | 86 | buf := bufio.NewReader(conn) 87 | buf.ReadString('\n') //read welcome message 88 | err = SendCommand(conn, cmd) 89 | if err != nil { 90 | return "", err 91 | } 92 | 93 | return ReadResponse(buf) 94 | } 95 | -------------------------------------------------------------------------------- /server/mi/command.go: -------------------------------------------------------------------------------- 1 | package mi 2 | 3 | import ( 4 | "bufio" 5 | "fmt" 6 | "net" 7 | "strings" 8 | 9 | log "github.com/Sirupsen/logrus" 10 | ) 11 | 12 | //SendCommand passes command to a given connection (adds logging and EOL character) 13 | func SendCommand(conn net.Conn, cmd string) error { 14 | log.Debug("Sending command: " + cmd) 15 | if _, err := fmt.Fprintf(conn, cmd+"\n"); err != nil { 16 | return err 17 | } 18 | log.Debug("Command ", cmd, " successfuly send") 19 | return nil 20 | } 21 | 22 | //ReadResponse . 23 | func ReadResponse(reader *bufio.Reader) (string, error) { 24 | var finished = false 25 | var result = "" 26 | i := 0 27 | 28 | for finished == false { 29 | line, err := reader.ReadString('\n') 30 | if err != nil { 31 | log.Error(line, err) 32 | return "", err 33 | } 34 | 35 | result += line 36 | if strings.Index(line, "END") == 0 || 37 | strings.Index(line, "SUCCESS:") == 0 || 38 | strings.Index(line, "ERROR:") == 0 { 39 | finished = true 40 | } 41 | i++ 42 | } 43 | return result, nil 44 | } 45 | -------------------------------------------------------------------------------- /server/mi/command_test.go: -------------------------------------------------------------------------------- 1 | package mi_test 2 | 3 | import ( 4 | "bufio" 5 | "net" 6 | "strings" 7 | "testing" 8 | 9 | mi "github.com/adamwalach/go-openvpn/server/mi" 10 | "github.com/stretchr/testify/assert" 11 | ) 12 | 13 | var cResponsePid = `SUCCESS: pid=10869 14 | ` 15 | 16 | func TestReadResponsePid(t *testing.T) { 17 | reader := bufio.NewReader(strings.NewReader(cResponsePid)) 18 | response, err := mi.ReadResponse(reader) 19 | assert.Nil(t, err) 20 | pid, err := mi.ParsePid(response) 21 | assert.Nil(t, err) 22 | assert.Equal(t, int64(10869), pid) 23 | } 24 | 25 | func TestReadResponseFailure(t *testing.T) { 26 | reader := bufio.NewReader(strings.NewReader("")) 27 | _, err := mi.ReadResponse(reader) 28 | assert.NotNil(t, err) 29 | } 30 | 31 | func TestSendCommandFailure(t *testing.T) { 32 | conn := &net.IPConn{} 33 | err := mi.SendCommand(conn, "dummy") 34 | assert.NotNil(t, err) 35 | } 36 | -------------------------------------------------------------------------------- /server/mi/parse.go: -------------------------------------------------------------------------------- 1 | package mi 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | "strconv" 7 | "strings" 8 | ) 9 | 10 | //ParsePid gets pid from string 11 | func ParsePid(input string) (int64, error) { 12 | a := strings.Split(trim(input), "\n") 13 | if len(a) != 1 { 14 | return int64(0), fmt.Errorf("Wrong number of lines, expected %d, got %d", 1, len(a)) 15 | } 16 | if !isSuccess(a[0]) { 17 | return int64(0), fmt.Errorf("Bad response: %s", a[0]) 18 | } 19 | return strconv.ParseInt(stripPrefix(a[0], "SUCCESS: pid="), 10, 64) 20 | } 21 | 22 | //ParseVersion gets version information from string 23 | func ParseVersion(input string) (*Version, error) { 24 | v := Version{} 25 | a := strings.Split(trim(input), "\n") 26 | if len(a) != 3 { 27 | return nil, fmt.Errorf("Wrong number of lines, expected %d, got %d", 3, len(a)) 28 | } 29 | v.OpenVPN = stripPrefix(a[0], "OpenVPN Version: ") 30 | v.Management = stripPrefix(a[1], "Management Version: ") 31 | 32 | return &v, nil 33 | } 34 | 35 | //ParseStats gets stats from string 36 | func ParseStats(input string) (*LoadStats, error) { 37 | ls := LoadStats{} 38 | a := strings.Split(trim(input), "\n") 39 | 40 | if len(a) != 1 { 41 | return nil, fmt.Errorf("Wrong number of lines, expected %d, got %d", 1, len(a)) 42 | } 43 | line := a[0] 44 | if !isSuccess(line) { 45 | return nil, fmt.Errorf("Bad response: %s", line) 46 | } 47 | 48 | dString := stripPrefix(line, "SUCCESS: ") 49 | dElements := strings.Split(dString, ",") 50 | var err error 51 | ls.NClients, err = getLStatsValue(dElements[0]) 52 | if err != nil { 53 | return nil, err 54 | } 55 | ls.BytesIn, err = getLStatsValue(dElements[1]) 56 | if err != nil { 57 | return nil, err 58 | } 59 | ls.BytesOut, err = getLStatsValue(dElements[2]) 60 | if err != nil { 61 | return nil, err 62 | } 63 | return &ls, nil 64 | } 65 | 66 | //ParseStatus gets status information from string 67 | func ParseStatus(input string) (*Status, error) { 68 | s := Status{} 69 | s.ClientList = make([]*OVClient, 0, 0) 70 | s.RoutingTable = make([]*RoutingPath, 0, 0) 71 | a := strings.Split(trim(input), "\n") 72 | for _, line := range a { 73 | fields := strings.Split(trim(line), ",") 74 | c := fields[0] 75 | switch { 76 | case c == "TITLE": 77 | s.Title = fields[1] 78 | case c == "TIME": 79 | s.Time = fields[1] 80 | s.TimeT = fields[2] 81 | case c == "ROUTING_TABLE": 82 | item := &RoutingPath{ 83 | VirtualAddress: fields[1], 84 | CommonName: fields[2], 85 | RealAddress: fields[3], 86 | LastRef: fields[4], 87 | LastRefT: fields[5], 88 | } 89 | s.RoutingTable = append(s.RoutingTable, item) 90 | case c == "CLIENT_LIST": 91 | bytesR, _ := strconv.ParseUint(fields[4], 10, 64) 92 | bytesS, _ := strconv.ParseUint(fields[5], 10, 64) 93 | item := &OVClient{ 94 | CommonName: fields[1], 95 | RealAddress: fields[2], 96 | VirtualAddress: fields[3], 97 | BytesReceived: bytesR, 98 | BytesSent: bytesS, 99 | ConnectedSince: fields[6], 100 | ConnectedSinceT: fields[7], 101 | Username: fields[8], 102 | } 103 | s.ClientList = append(s.ClientList, item) 104 | } 105 | } 106 | return &s, nil 107 | } 108 | 109 | //ParseSignal checks for error in response string 110 | func ParseSignal(input string) error { 111 | a := strings.Split(trim(input), "\n") 112 | if len(a) != 1 { 113 | return fmt.Errorf("Wrong number of lines, expected %d, got %d", 1, len(a)) 114 | } 115 | if !isSuccess(a[0]) { 116 | return fmt.Errorf("Bad response: %s", a[0]) 117 | } 118 | return nil 119 | } 120 | 121 | //ParseKillSession gets kill command result from string 122 | func ParseKillSession(input string) (string, error) { 123 | a := strings.Split(trim(input), "\n") 124 | 125 | if len(a) != 1 { 126 | return "", fmt.Errorf("Wrong number of lines, expected %d, got %d", 1, len(a)) 127 | } 128 | line := a[0] 129 | if !isSuccess(line) { 130 | return "", errors.New(line) 131 | } 132 | 133 | return stripPrefix(line, "SUCCESS: "), nil 134 | } 135 | 136 | func getLStatsValue(s string) (int64, error) { 137 | a := strings.Split(s, "=") 138 | if len(a) != 2 { 139 | return int64(-1), errors.New("Parsing error") 140 | } 141 | return strconv.ParseInt(a[1], 10, 64) 142 | } 143 | 144 | func trim(s string) string { 145 | return strings.Trim(strings.Trim(s, "\r\n"), "\n") 146 | } 147 | 148 | func stripPrefix(s, prefix string) string { 149 | return trim(strings.Replace(s, prefix, "", 1)) 150 | } 151 | 152 | func isSuccess(s string) bool { 153 | if strings.HasPrefix(s, "SUCCESS: ") { 154 | return true 155 | } 156 | return false 157 | } 158 | -------------------------------------------------------------------------------- /server/mi/parse_test.go: -------------------------------------------------------------------------------- 1 | package mi_test 2 | 3 | import ( 4 | "testing" 5 | 6 | mi "github.com/adamwalach/go-openvpn/server/mi" 7 | "github.com/stretchr/testify/assert" 8 | ) 9 | 10 | var responseVersion = `OpenVPN Version: OpenVPN 2.3.2 x86_64-pc-linux-gnu [SSL (OpenSSL)] [LZO] [EPOLL] [PKCS11] [eurephia] [MH] [IPv6] built on Dec 1 2014 11 | Management Version: 1 12 | END 13 | ` 14 | 15 | var responsePid = `SUCCESS: pid=10869 16 | ` 17 | 18 | var responseError = `Error: bad response 19 | ` 20 | var responseLoadStats = `SUCCESS: nclients=12,bytesin=109984474,bytesout=1589364037 21 | ` 22 | var responseLoadStatsBroken = `SUCCESS: nclients=12,bytesin,bytesout=1589364037 23 | ` 24 | var responseStatus = `TITLE,OpenVPN 2.3.2 x86_64-pc-linux-gnu [SSL (OpenSSL)] [LZO] [EPOLL] [PKCS11] [eurephia] [MH] [IPv6] built on Dec 1 2014 25 | TIME,Mon Dec 26 21:02:26 2016,1482782546 26 | HEADER,CLIENT_LIST,Common Name,Real Address,Virtual Address,Bytes Received,Bytes Sent,Connected Since,Connected Since (time_t),Username 27 | CLIENT_LIST,democlient1.example.com,11.112.113.114:58978,10.8.0.30,131180,414251,Mon Dec 26 20:57:17 2016,1482782237,UNDEF 28 | CLIENT_LIST,rpi-v3,11.112.113.114:35331,10.8.0.14,5107,6909,Mon Dec 26 20:59:42 2016,1482782382,UNDEF 29 | HEADER,ROUTING_TABLE,Virtual Address,Common Name,Real Address,Last Ref,Last Ref (time_t) 30 | ROUTING_TABLE,10.8.0.30,democlient1.example.com,11.112.113.114:58978,Mon Dec 26 21:02:25 2016,1482782545 31 | ROUTING_TABLE,10.8.0.14,rpi-v3,11.112.113.114:35331,Mon Dec 26 20:59:43 2016,1482782383 32 | GLOBAL_STATS,Max bcast/mcast queue length,0 33 | END 34 | ` 35 | var responseKillSession = `SUCCESS: common name 'test1.example.com' found, 1 client(s) killed 36 | ` 37 | var responseKillSessionError = `failure 38 | ` 39 | 40 | func TestVersionEmptyStr(t *testing.T) { 41 | _, err := mi.ParseVersion("") 42 | assert.NotNil(t, err) 43 | } 44 | 45 | func TestVersionParser(t *testing.T) { 46 | v, err := mi.ParseVersion(responseVersion) 47 | assert.Nil(t, err) 48 | 49 | openVpnExpected := "OpenVPN 2.3.2 x86_64-pc-linux-gnu [SSL (OpenSSL)] [LZO] [EPOLL] [PKCS11] [eurephia] [MH] [IPv6] built on Dec 1 2014" 50 | assert.Equal(t, openVpnExpected, v.OpenVPN) 51 | assert.Equal(t, "1", v.Management) 52 | } 53 | 54 | func TestPidEmptyStr(t *testing.T) { 55 | _, err := mi.ParsePid("") 56 | assert.NotNil(t, err) 57 | } 58 | 59 | func TestPidErrorStr(t *testing.T) { 60 | _, err := mi.ParsePid(responseError) 61 | assert.NotNil(t, err) 62 | } 63 | 64 | func TestPidWrongNrOfLines(t *testing.T) { 65 | _, err := mi.ParsePid("a\n\n\na") 66 | assert.NotNil(t, err) 67 | } 68 | 69 | func TestPidParser(t *testing.T) { 70 | pid, err := mi.ParsePid(responsePid) 71 | assert.Nil(t, err) 72 | assert.Equal(t, int64(10869), pid) 73 | } 74 | 75 | func TestLoadStatsEmptyStr(t *testing.T) { 76 | _, err := mi.ParseStats("") 77 | assert.NotNil(t, err) 78 | } 79 | 80 | func TestLoadStatsWrongNrOfLines(t *testing.T) { 81 | _, err := mi.ParseStats("a\n\na") 82 | assert.NotNil(t, err) 83 | } 84 | 85 | func TestStatsParser(t *testing.T) { 86 | ls, err := mi.ParseStats(responseLoadStats) 87 | assert.Nil(t, err) 88 | assert.Equal(t, int64(12), ls.NClients) 89 | assert.Equal(t, int64(109984474), ls.BytesIn) 90 | assert.Equal(t, int64(1589364037), ls.BytesOut) 91 | } 92 | 93 | func TestStatsParserError(t *testing.T) { 94 | _, err := mi.ParseStats(responseLoadStatsBroken) 95 | assert.NotNil(t, err) 96 | } 97 | 98 | func TestStatsErrorStr(t *testing.T) { 99 | _, err := mi.ParseStats(responseError) 100 | assert.NotNil(t, err) 101 | } 102 | 103 | func TestStatusParser(t *testing.T) { 104 | s, err := mi.ParseStatus(responseStatus) 105 | assert.Nil(t, err) 106 | sExpected := &mi.Status{ 107 | Title: "OpenVPN 2.3.2 x86_64-pc-linux-gnu [SSL (OpenSSL)] [LZO] [EPOLL] [PKCS11] [eurephia] [MH] [IPv6] built on Dec 1 2014", 108 | Time: "Mon Dec 26 21:02:26 2016", 109 | TimeT: "1482782546", 110 | ClientList: []*mi.OVClient{ 111 | { 112 | CommonName: "democlient1.example.com", 113 | RealAddress: "11.112.113.114:58978", 114 | VirtualAddress: "10.8.0.30", 115 | BytesReceived: 131180, 116 | BytesSent: 414251, 117 | ConnectedSince: "Mon Dec 26 20:57:17 2016", 118 | ConnectedSinceT: "1482782237", 119 | Username: "UNDEF", 120 | }, 121 | { 122 | CommonName: "rpi-v3", 123 | RealAddress: "11.112.113.114:35331", 124 | VirtualAddress: "10.8.0.14", 125 | BytesReceived: 5107, 126 | BytesSent: 6909, 127 | ConnectedSince: "Mon Dec 26 20:59:42 2016", 128 | ConnectedSinceT: "1482782382", 129 | Username: "UNDEF", 130 | }, 131 | }, 132 | RoutingTable: []*mi.RoutingPath{ 133 | { 134 | VirtualAddress: "10.8.0.30", 135 | CommonName: "democlient1.example.com", 136 | RealAddress: "11.112.113.114:58978", 137 | LastRef: "Mon Dec 26 21:02:25 2016", 138 | LastRefT: "1482782545", 139 | }, 140 | { 141 | VirtualAddress: "10.8.0.14", 142 | CommonName: "rpi-v3", 143 | RealAddress: "11.112.113.114:35331", 144 | LastRef: "Mon Dec 26 20:59:43 2016", 145 | LastRefT: "1482782383", 146 | }, 147 | }, 148 | } 149 | assert.Equal(t, sExpected, s) 150 | } 151 | 152 | func TestKillSessionParser(t *testing.T) { 153 | m, err := mi.ParseKillSession(responseKillSession) 154 | assert.Nil(t, err) 155 | assert.Equal(t, "common name 'test1.example.com' found, 1 client(s) killed", 156 | m) 157 | } 158 | 159 | func TestKillSessionWrongNrOfLines(t *testing.T) { 160 | _, err := mi.ParseKillSession("a\n\na") 161 | assert.NotNil(t, err) 162 | } 163 | 164 | func TestKillSessionResponseError(t *testing.T) { 165 | _, err := mi.ParseKillSession(responseKillSessionError) 166 | assert.NotNil(t, err) 167 | } 168 | -------------------------------------------------------------------------------- /server/mi/types.go: -------------------------------------------------------------------------------- 1 | package mi 2 | 3 | //Version . 4 | type Version struct { 5 | OpenVPN string 6 | Management string 7 | } 8 | 9 | //LoadStats . 10 | type LoadStats struct { 11 | NClients int64 12 | BytesIn int64 13 | BytesOut int64 14 | } 15 | 16 | //Status . 17 | type Status struct { 18 | Title string 19 | Time string 20 | TimeT string 21 | ClientList []*OVClient 22 | RoutingTable []*RoutingPath 23 | } 24 | 25 | //OVClient . 26 | type OVClient struct { 27 | CommonName string 28 | RealAddress string 29 | VirtualAddress string 30 | BytesReceived uint64 31 | BytesSent uint64 32 | ConnectedSince string 33 | ConnectedSinceT string 34 | Username string 35 | } 36 | 37 | //RoutingPath . 38 | type RoutingPath struct { 39 | VirtualAddress string 40 | CommonName string 41 | RealAddress string 42 | LastRef string 43 | LastRefT string 44 | } 45 | --------------------------------------------------------------------------------