├── .circleci └── config.yml ├── README.md ├── main.go ├── tcppc.service.orig ├── tcppc.toml.orig └── tcppc ├── counter.go ├── handler.go ├── session.go ├── tcp.go ├── tls.go ├── udp.go └── writer.go /.circleci/config.yml: -------------------------------------------------------------------------------- 1 | # Golang CircleCI 2.0 configuration file 2 | # 3 | # Check https://circleci.com/docs/2.0/language-go/ for more details 4 | version: 2 5 | jobs: 6 | build: 7 | docker: 8 | - image: circleci/golang:1.11 9 | working_directory: /go/src/github.com/md-irohas/tcppc-go 10 | steps: 11 | - checkout 12 | - run: go get -v -d 13 | - run: go build -v 14 | - store_artifacts: 15 | path: ./tcppc-go 16 | - persist_to_workspace: 17 | root: ./ 18 | paths: 19 | - tcppc-go 20 | 21 | deploy-to-github-release: 22 | docker: 23 | - image: circleci/golang:1.11 24 | steps: 25 | - attach_workspace: 26 | at: /tmp/build/ 27 | - run: 28 | name: "Publish Release on GitHub" 29 | command: | 30 | go get github.com/tcnksm/ghr 31 | VERSION=$(/tmp/build/tcppc-go -v) 32 | ghr -t ${GITHUB_TOKEN} -u ${CIRCLE_PROJECT_USERNAME} -r ${CIRCLE_PROJECT_REPONAME} -c ${CIRCLE_SHA1} -delete ${VERSION} /tmp/build/ 33 | 34 | workflows: 35 | version: 2 36 | build: 37 | jobs: 38 | - build 39 | build_and_deploy: 40 | jobs: 41 | - build: 42 | filters: 43 | tags: 44 | only: /^\d+\.\d+\.\d+$/ 45 | branches: 46 | ignore: /.*/ 47 | - deploy-to-github-release: 48 | requires: 49 | - build 50 | filters: 51 | tags: 52 | only: /^\d+\.\d+\.\d+$/ 53 | branches: 54 | ignore: /.*/ 55 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # tcppc 2 | 3 | `tcppc` is a simple honeypot program to capture TCP/TLS/UDP payloads. This 4 | program listens on the given IP address and the given port, establishes 5 | connections from external hosts, and continues to receive packets until the 6 | connections are closed or timeouted. `tcppc` supports transparent proxy 7 | (TPROXY) by iptables, so you can get payloads of arbitrary ports using `tcppc`. 8 | I am developing this program to use as a honeypot for monitoring payloads. 9 | 10 | Main functions: 11 | 12 | * Establish TCP or TLS handshake and continue to receive packets. 13 | * Receive UDP packets. 14 | * Support transparent proxy (PROXY) to listen on all ports. 15 | * Save received data (session data) as JSON lines format. 16 | * Rotate the data files in the given interval. 17 | 18 | 19 | ## Installation 20 | 21 | ### Precompiled binary 22 | 23 | Precompiled binaries for Linux (x86_64) are released. 24 | See [release](https://github.com/md-irohas/tcppc-go/releases) page. 25 | 26 | ### Compile from source 27 | 28 | `tcppc` is written in Go. So, if you want to build its binary, you need to 29 | prepare the development environment for Go. 30 | 31 | If you are ready for building Go, type the following commands. 32 | 33 | ```sh 34 | $ go get github.com/md-irohas/tcppc-go 35 | ``` 36 | 37 | Note that the version of go compiler must be 1.11.0 or newer to enable timeout 38 | of connection. 39 | 40 | 41 | ## Usage 42 | 43 | The followings are the options of `tcppc`. 44 | You can also use configuration files instead of using these options (See 45 | 'Configuration' section). 46 | 47 | ```sh 48 | Usage of ./tcppc-go: 49 | -C string 50 | TLS certificate file. 51 | -H string 52 | hostname to listen on. (default "0.0.0.0") 53 | -K string 54 | TLS key file. 55 | -L string 56 | [deprecated] log file. 57 | -R uint 58 | maximum number of file descriptors (need root priviledge). 59 | -T int 60 | rotation interval [sec]. 61 | -c string 62 | configuration file. 63 | -disable-tcp-server 64 | disable TCP/TLS server. 65 | -disable-udp-server 66 | disable UDP server. 67 | -offset int 68 | rotation interval offset [sec]. 69 | -p int 70 | port number to listen on. (default 12345) 71 | -t int 72 | timeout for TCP/TLS connection. (default 60) 73 | -v show version and exit. 74 | -w string 75 | session file (JSON lines format). 76 | -z string 77 | timezone used for session file. (default "Local") 78 | ``` 79 | 80 | 81 | ### Example-1: Basics 82 | 83 | Run tcppc-go program. 84 | 85 | ```sh 86 | $ sudo ./tcppc-go 87 | 2019/04/16 23:42:32 Maximum number of file descriptors: 1024 88 | 2019/04/16 23:42:32 Timezone: Local 89 | 2019/04/16 23:42:32 Timeout: 60 90 | 2019/04/16 23:42:32 Session data file: none. 91 | 2019/04/16 23:42:32 !!!CAUTION!!! Session data will not be written to files. 92 | 2019/04/16 23:42:32 Server Mode: TCP 93 | 2019/04/16 23:42:32 Listen: 0.0.0.0:12345 94 | 2019/04/16 23:42:32 Start TCP server. 95 | 2019/04/16 23:42:32 Server Mode: UDP 96 | 2019/04/16 23:42:32 Listen: 0.0.0.0:12345 97 | 2019/04/16 23:42:32 Start UDP server. 98 | ``` 99 | 100 | Connect to the server from another terminal. 101 | 102 | ```sh 103 | $ echo "Hello, TCPPC" | nc 127.0.0.1 12345 104 | ``` 105 | 106 | The tcppc-go gets the following logs. 107 | 108 | ```sh 109 | $ ./tcppc-go 110 | ... 111 | 2019/04/16 23:44:00 TCP: Established: Session: 2019-04-16T23:44:00: Flow: tcp 127.0.0.1:60998 <-> 127.0.0.1:12345 (0 payloads) (#Sessions: 1) 112 | 2019/04/16 23:44:00 TCP: Received: Session: 2019-04-16T23:44:00: Flow: tcp 127.0.0.1:60998 <-> 127.0.0.1:12345 (1 payloads): "Hello, TCPPC\n" (13 bytes) 113 | 2019/04/16 23:44:00 Closed: Session: 2019-04-16T23:44:00: Flow: tcp 127.0.0.1:60998 <-> 127.0.0.1:12345 (1 payloads) (#Sessions: 1) 114 | ``` 115 | 116 | Send UDP packets from the terminal. 117 | 118 | ```sh 119 | $ echo "Hello, TCPPC" | nc -u 127.0.0.1 12345 120 | ``` 121 | 122 | The tcppc-go gets the following logs. 123 | 124 | ```sh 125 | ... 126 | 2019/04/16 23:45:20 UDP: Received: Session: 2019-04-16T23:45:20: Flow: udp 127.0.0.1:49616 <-> 127.0.0.1:12345 (1 payloads): "Hello, TCPPC\n" (13 bytes) 127 | ``` 128 | 129 | Type Ctrl+C to stop this program. 130 | 131 | ### Example-2: Save session data 132 | 133 | You can save session data to files as [JSON lines](http://jsonlines.org/) format. 134 | 135 | When `-w` option is specified, the data will be written to the given file. 136 | You can use datetime format in `-w` option (See `man strftime` for more 137 | details). When `-T` option is specified, data files will be rotated every 138 | given seconds. 139 | 140 | Run tcppc-go program. 141 | 142 | ```sh 143 | $ ./tcppc-go -T 86400 -w log/tcppc-%Y%m%d.jsonl 144 | ``` 145 | 146 | Connect to the server from another terminal. 147 | 148 | ```sh 149 | $ echo "Hello, TCPPC" | nc 127.0.0.1 12345 150 | ``` 151 | 152 | The results of the data are the following. 153 | Note that data in payloads are encoded in base64. 154 | 155 | ```sh 156 | # jq is a command for formatting JSON. 157 | # I tested this on April 18th, 2018. 158 | 159 | $ jq . log/tcppc-20180418.jsonl 160 | { 161 | "timestamp": "2018-04-18T09:51:34.689896842+09:00", 162 | "flow": { 163 | "proto": "tcp", 164 | "src": "127.0.0.1", 165 | "sport": 53484, 166 | "dst": "127.0.0.1", 167 | "dport": 12345 168 | }, 169 | "payloads": [ 170 | { 171 | "index": 0, 172 | "timestamp": "2018-04-18T09:51:34.690076698+09:00", 173 | "data": "SGVsbG8sIFRDUFBDCg==" 174 | } 175 | ] 176 | } 177 | ``` 178 | 179 | ### Example-3: TLS handshaker 180 | 181 | `tcppc` supports not only TCP handshake but also TLS handshake. 182 | 183 | When both `-C` and `-K` options are specified, this program works as TLS 184 | handshaker. You need to prepare for TLS certificate (in many cases, 185 | self-signed) and key files (See 'Configuration' section). 186 | 187 | Run tcppc-go program. 188 | 189 | ```sh 190 | $ ./tcppc-go -T 86400 -C server.crt -K server.key -w log/tcppc-%Y%m%d.jsonl 191 | ``` 192 | 193 | Connect to the server from another terminal. 194 | 195 | ```sh 196 | $ wget --no-check-certificate https://127.0.0.1:12345/index 197 | ``` 198 | 199 | The results of session data are the following (formatted by `jq` command). 200 | 201 | ```sh 202 | $ jq . log/tcppc-20180418.jsonl 203 | { 204 | "timestamp": "2018-04-18T10:06:08.104667676+09:00", 205 | "flow": { 206 | "proto": "tls", 207 | "src": "127.0.0.1", 208 | "sport": 53635, 209 | "dst": "127.0.0.1", 210 | "dport": 12345 211 | }, 212 | "payloads": [ 213 | { 214 | "index": 0, 215 | "timestamp": "2018-04-18T10:06:08.111138967+09:00", 216 | "data": "R0VUIC9pbmRleCBIVFRQLzEuMQ0KVXNlci1BZ2VudDogV2dldC8xLjE5LjIgKGRhcndpbjE3LjMuMCkNCkFjY2VwdDogKi8qDQpBY2NlcHQtRW5jb2Rpbmc6IGd6aXANCkhvc3Q6IDEyNy4wLjAuMToxMjM0NQ0KQ29ubmVjdGlvbjogS2VlcC1BbGl2ZQ0KDQo=" 217 | } 218 | ] 219 | } 220 | 221 | # decode "data" as base64. 222 | $ echo "R0VUIC9pbmRleCBIVFRQLzEuMQ0KVXNlci1BZ2VudDogV2dldC8xLjE5LjIgKGRhcndpbjE3LjMuMCkNCkFjY2VwdDogKi8qDQpBY2NlcHQtRW5jb2Rpbmc6IGd6aXANCkhvc3Q6IDEyNy4wLjAuMToxMjM0NQ0KQ29ubmVjdGlvbjogS2VlcC1BbGl2ZQ0KDQo=" | base64 -D 223 | GET /index HTTP/1.1 224 | User-Agent: Wget/1.19.2 (darwin17.3.0) 225 | Accept: */* 226 | Accept-Encoding: gzip 227 | Host: 127.0.0.1:12345 228 | Connection: Keep-Alive 229 | 230 | ``` 231 | 232 | 233 | ## Configuration 234 | 235 | ### Configuration file 236 | 237 | The template of configuration file in [TOML](https://github.com/toml-lang/toml) format is ready. 238 | See tcppc.toml.orig. 239 | 240 | ```sh 241 | $ cp tcppc.toml.orig /etc/tcppc.toml 242 | $ vim /etc/tcppc.toml 243 | 244 | ... (edit) ... 245 | ``` 246 | 247 | ### TLS certificate/key files 248 | 249 | If you want to use `tcppc` as TLS handshaker, you need to prepare TLS 250 | certificate file and TLS key file. 251 | 252 | You can create these files by the following commands. 253 | 254 | ```sh 255 | $ openssl genrsa 2048 > server.key 256 | $ openssl req -new -key server.key > server.csr 257 | $ openssl x509 -days 36500 -req -signkey server.key < server.csr > server.crt 258 | ``` 259 | 260 | Note that these commands create not a valid certificate file but a 261 | self-signed certificate file. 262 | 263 | ### Systemd 264 | 265 | A simple unit file of systemd is ready (`tcppc.service.orig`) 266 | Edit it and enable/start `tcppc` service. 267 | 268 | ```sh 269 | # copy this file to systemd's directory. 270 | cp -v tcppc.service.orig /etc/systemd/system/ 271 | 272 | # edit this file. 273 | vim /etc/systemd/system/tcppc.service 274 | 275 | # reload unit files. 276 | systemctl daemon-reload 277 | 278 | # start tcppc service. 279 | systemctl start tcppc 280 | 281 | # (optional) autostart tcppc service 282 | systemctl enable tcppc 283 | ``` 284 | 285 | ### Listen on all ports 286 | 287 | The easiest way to listen on all ports is to use TPROXY function of `iptables`. 288 | 289 | In this case, you should prepare a new (pseudo) network interface and an IP 290 | address (i.e. IP alias) to monitor and capture all the traffic. 291 | 292 | ```sh 293 | # !!! DANGER !!! 294 | $ iptables -t mangle -A PREROUTING -i -p tcp -d -j TPROXY --tproxy-mark 0x1/0x1 --on-ip --on-port 12345 295 | $ iptables -t mangle -A PREROUTING -i -p udp -d -j TPROXY --tproxy-mark 0x1/0x1 --on-ip --on-port 12345 296 | ``` 297 | 298 | 299 | ## Session data format 300 | 301 | Session data file is [JSON Lines](http://jsonlines.org/) format (i.e. each 302 | line holds a JSON string.) 303 | Each line represents each session data. 304 | 305 | The following shows the data example with some comments. 306 | 307 | ``` 308 | { 309 | // Time when the session is accepted. 310 | // i.e. 311 | // tcp/tls: time when the handshake is finished. 312 | // udp: time when the UDP packet is received. 313 | "timestamp": "2018-04-18T11:06:09.419437117+09:00", 314 | 315 | // Flow (protocol (i.e., tcp/tls/udp, source IP address, source port, local address, local port) 316 | "flow": { 317 | "proto": "tcp", 318 | "src": "127.0.0.1", 319 | "sport": 54167, 320 | "dst": "127.0.0.1", 321 | "dport": 12345 322 | }, 323 | 324 | // List of payloads 325 | "payloads": [ 326 | { 327 | // Index of payloads 328 | "index": 0, 329 | 330 | // Time when this payload was received. 331 | "timestamp": "2018-04-18T11:06:13.830444868+09:00", 332 | 333 | // Data encoded in base64 334 | "data": "Rmlyc3QgcGF5bG9hZAo=" 335 | }, 336 | { 337 | "index": 1, 338 | "timestamp": "2018-04-18T11:06:18.015019663+09:00", 339 | "data": "U2Vjb25kIHBheWxvYWQK" 340 | } 341 | ] 342 | } 343 | ``` 344 | 345 | 346 | ## Alternatives 347 | 348 | You might be able to use `nc` or `socat` instead. 349 | 350 | 351 | ## License 352 | 353 | MIT License ([link](https://opensource.org/licenses/MIT)). 354 | 355 | 356 | ## Contact 357 | 358 | md (E-mail: md.irohas at gmail.com) 359 | 360 | 361 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "crypto/tls" 5 | "flag" 6 | "fmt" 7 | "github.com/md-irohas/tcppc-go/tcppc" 8 | "github.com/pelletier/go-toml" 9 | "log" 10 | "os" 11 | "os/signal" 12 | "runtime" 13 | "syscall" 14 | "time" 15 | ) 16 | 17 | const ( 18 | Version = "0.4.0" 19 | ) 20 | 21 | var ( 22 | host = flag.String("H", "0.0.0.0", "hostname to listen on.") 23 | port = flag.Int("p", 12345, "port number to listen on.") 24 | timeout = flag.Int("t", 60, "timeout for TCP/TLS connection.") 25 | fileNameFmt = flag.String("w", "", "session file (JSON lines format).") 26 | rotInt = flag.Int("T", 0, "rotation interval [sec].") 27 | rotOffset = flag.Int("offset", 0, "rotation interval offset [sec].") 28 | logFileName = flag.String("L", "", "[deprecated] log file.") 29 | timezone = flag.String("z", "Local", "timezone used for session file.") 30 | maxFdNum = flag.Uint64("R", 0, "maximum number of file descriptors (need root priviledge).") 31 | x509Cert = flag.String("C", "", "TLS certificate file.") 32 | x509Key = flag.String("K", "", "TLS key file.") 33 | cnfFileName = flag.String("c", "", "configuration file.") 34 | disableTcpServer = flag.Bool("disable-tcp-server", false, "disable TCP/TLS server.") 35 | disableUdpServer = flag.Bool("disable-udp-server", false, "disable UDP server.") 36 | showVersion = flag.Bool("v", false, "show version and exit.") 37 | ) 38 | 39 | func main() { 40 | flag.Parse() 41 | if *showVersion { 42 | fmt.Println(Version) 43 | return 44 | } 45 | 46 | log.SetFlags(log.LstdFlags) 47 | 48 | // Linux only. 49 | if runtime.GOOS != "linux" { 50 | log.Fatalf("This program runs only in Linux.") 51 | } 52 | 53 | // Parse params from config file. 54 | // Params in the command-line arguments are ignored. 55 | if *cnfFileName != "" { 56 | cnf, err := toml.LoadFile(*cnfFileName) 57 | if err != nil { 58 | log.Fatalf("Failed to load configuration file: %s", err) 59 | } 60 | 61 | *host = cnf.Get("tcppc.host").(string) 62 | *port = int(cnf.Get("tcppc.port").(int64)) 63 | *timeout = int(cnf.Get("tcppc.timeout").(int64)) 64 | *fileNameFmt = cnf.Get("tcppc.tcpFileFmt").(string) 65 | *rotInt = int(cnf.Get("tcppc.rotInt").(int64)) 66 | *rotOffset = int(cnf.Get("tcppc.rotOffset").(int64)) 67 | *logFileName = cnf.Get("tcppc.logFile").(string) 68 | *timezone = cnf.Get("tcppc.timezone").(string) 69 | *maxFdNum = uint64(cnf.Get("tcppc.maxFdNum").(int64)) 70 | *x509Cert = cnf.Get("tcppc.x509Cert").(string) 71 | *x509Key = cnf.Get("tcppc.x509Key").(string) 72 | } 73 | 74 | // This log file is deprecated. 75 | // The default logging package of goland does not support a function to 76 | // rotate log files. Therefore, the log file of this process will consume 77 | // huge diskspace. If your OS uses systemd-journald, it manages the 78 | // stdout/stderr of this process, so you should use it instead. 79 | if *logFileName != "" { 80 | f, err := os.OpenFile(*logFileName, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0640) 81 | if err != nil { 82 | log.Fatalf("Failed to open log file: %s\n", err) 83 | } 84 | 85 | log.SetOutput(f) 86 | log.Printf("Open log file: %s\n", logFileName) 87 | } 88 | 89 | // Raise the upper limit of the number of file descriptors to handle many 90 | // requests such as port scannings by attackers. 91 | var rLimit syscall.Rlimit 92 | if *maxFdNum > 0 { 93 | rLimit.Max = *maxFdNum 94 | rLimit.Cur = *maxFdNum 95 | 96 | err := syscall.Setrlimit(syscall.RLIMIT_NOFILE, &rLimit) 97 | if err != nil { 98 | log.Fatalf("Failed to set maximum number of file descriptors: %s\n", err) 99 | } 100 | } 101 | 102 | err := syscall.Getrlimit(syscall.RLIMIT_NOFILE, &rLimit) 103 | if err != nil { 104 | log.Fatalf("Failed to get maximum number of file descriptos.\n") 105 | } 106 | 107 | log.Printf("Maximum number of file descriptors: %d\n", rLimit.Cur) 108 | 109 | // Load location from timezone. 110 | // This location object is used to determine the filename of tcp session 111 | // files by RotWriter. 112 | loc, err := time.LoadLocation(*timezone) 113 | if err != nil { 114 | log.Fatalf("Failed to load timezone: %s %s\n", *timezone, err) 115 | } 116 | 117 | log.Printf("Timezone: %s\n", *timezone) 118 | log.Printf("Timeout: %d\n", *timeout) 119 | 120 | // Select mode of tcppc. 121 | // When both TLS certificate file and TLS key file are given, this program 122 | // starts listening as TLS handshaker. When none of them are given, this 123 | // program starts listening as TCP handshaker. Otherwise, this program 124 | // fails to start listening. 125 | var tcppcMode string 126 | if !*disableTcpServer { 127 | if *x509Cert != "" && *x509Key != "" { 128 | tcppcMode = "tls" 129 | } else if *x509Cert == "" && *x509Key == "" { 130 | tcppcMode = "tcp" 131 | } else { 132 | log.Println("Either TLS cerfiticate or key file is given.") 133 | log.Println("TCP handshaker: neither TLS certificate nor TLS key files are required.") 134 | log.Println("TLS handshaker: both TLS certificate and TLS key files are required.") 135 | log.Fatalln("Abort.") 136 | } 137 | } 138 | 139 | var writer *tcppc.RotWriter 140 | if *fileNameFmt != "" { 141 | log.Printf("Session data file: %s (Rotate every %d seconds w/ %d seconds offset)\n", *fileNameFmt, *rotInt, *rotOffset) 142 | 143 | writer = tcppc.NewWriter(*fileNameFmt, *rotInt, *rotOffset, loc) 144 | defer writer.Close() 145 | } else { 146 | log.Printf("Session data file: none.\n") 147 | log.Printf("!!!CAUTION!!! Session data will not be written to files.\n") 148 | 149 | writer = nil 150 | } 151 | 152 | if !*disableTcpServer { 153 | switch tcppcMode { 154 | case "tcp": 155 | go tcppc.StartTCPServer(*host, *port, writer, *timeout) 156 | 157 | case "tls": 158 | log.Printf("Certificate: %s, Key: %s\n", *x509Cert, *x509Key) 159 | 160 | cer, err := tls.LoadX509KeyPair(*x509Cert, *x509Key) 161 | if err != nil { 162 | log.Fatalf("Failed to load X509 key pair: %s\n", err) 163 | } 164 | 165 | config := &tls.Config{ 166 | Certificates: []tls.Certificate{cer}, 167 | } 168 | 169 | go tcppc.StartTLSServer(*host, *port, config, writer, *timeout) 170 | default: 171 | log.Fatalf("Unknown mode of tcppc: %s\n", tcppcMode) 172 | } 173 | } 174 | 175 | // Wait for TCP/TLS server to start, then start UDP server. 176 | if !*disableUdpServer { 177 | time.Sleep(100 * time.Millisecond) 178 | go tcppc.StartUDPServer(*host, *port, writer) 179 | } 180 | 181 | // Wait for SIGNAL. 182 | sigc := make(chan os.Signal, 1) 183 | signal.Notify(sigc, syscall.SIGHUP, syscall.SIGINT, syscall.SIGTERM) 184 | 185 | select { 186 | case <-sigc: 187 | log.Printf("Exit.") 188 | } 189 | } 190 | -------------------------------------------------------------------------------- /tcppc.service.orig: -------------------------------------------------------------------------------- 1 | # SYSTEMD unit file. 2 | 3 | [Unit] 4 | Description=TCP payload capture 5 | After=network.target 6 | 7 | [Service] 8 | Type=simple 9 | ExecStart=/usr/local/bin/tcppc -c /etc/tcppc.toml 10 | ExecStop=/bin/kill ${MAINPID} 11 | Restart=on-failure 12 | 13 | [Install] 14 | WantedBy=multi-user.target 15 | -------------------------------------------------------------------------------- /tcppc.toml.orig: -------------------------------------------------------------------------------- 1 | [tcppc] 2 | 3 | # hostname to listen on (e.g. 0.0.0.0). 4 | host = "0.0.0.0" 5 | 6 | # port number to listen on (e.g. 12345). 7 | port = 12345 8 | 9 | # timeout of TCP session in second. 10 | timeout = 60 11 | 12 | # filename format of TCP session data. 13 | # format of date and time (e.g. %Y, %m ...) will be converted (see man 14 | # strftime) 15 | tcpFileFmt = "data/tcppc-%Y%m%d.jsonl" 16 | 17 | # rotation interval in second. 18 | # session file will be rotated every `rotInt` seconds. if `rotInt` is zero, 19 | # the file won't be rotated forever. 20 | rotInt = 60 21 | 22 | # rotation interval offset in second. 23 | rotOffset = 0 24 | 25 | # [deprecated] log file for TCPPC program. 26 | logFile = "" 27 | 28 | # timezone for 'tcpFileFmt'. 29 | # see https://en.wikipedia.org/wiki/List_of_tz_database_time_zones 30 | timezone = "Local" 31 | 32 | # max number of file descriptors. 33 | maxFdNum = 8192 34 | 35 | # the following x509Cert and x509key are set, TCPPC will work as TLS 36 | # handshaker. If none of them are set, TCPPC will work as TCP handshaker. 37 | 38 | # TLS certificate file. 39 | x509Cert = "" 40 | 41 | # TLS key file. 42 | x509Key = "" 43 | -------------------------------------------------------------------------------- /tcppc/counter.go: -------------------------------------------------------------------------------- 1 | package tcppc 2 | 3 | import ( 4 | "sync" 5 | ) 6 | 7 | var ( 8 | counter = NewSessionCounter() 9 | ) 10 | 11 | type SessionCounter struct { 12 | Count uint 13 | mutex sync.RWMutex 14 | } 15 | 16 | func NewSessionCounter() *SessionCounter { 17 | return &SessionCounter{} 18 | } 19 | 20 | func (c *SessionCounter) inc() { 21 | c.mutex.Lock() 22 | defer c.mutex.Unlock() 23 | c.Count += 1 24 | } 25 | 26 | func (c *SessionCounter) dec() { 27 | c.mutex.Lock() 28 | defer c.mutex.Unlock() 29 | c.Count -= 1 30 | } 31 | 32 | func (c *SessionCounter) count() uint { 33 | c.mutex.Lock() 34 | defer c.mutex.Unlock() 35 | return c.Count 36 | } 37 | -------------------------------------------------------------------------------- /tcppc/handler.go: -------------------------------------------------------------------------------- 1 | package tcppc 2 | 3 | // 4 | // import ( 5 | // // "bufio" 6 | // "encoding/json" 7 | // "errors" 8 | // "log" 9 | // "os" 10 | // "net" 11 | // "runtime" 12 | // "sync" 13 | // "syscall" 14 | // "time" 15 | // "github.com/md-irohas/tcppc-go/crypto/tls" 16 | // ) 17 | // 18 | // const ( 19 | // SO_ORIGINAL_DST = 80 20 | // ) 21 | // 22 | // var ( 23 | // counter = NewSessionCounter() 24 | // OriginalDst = true 25 | // ) 26 | // 27 | // type SessionCounter struct { 28 | // Count uint 29 | // mutex sync.RWMutex 30 | // } 31 | // 32 | // func NewSessionCounter() *SessionCounter { 33 | // return &SessionCounter{} 34 | // } 35 | // 36 | // func (c *SessionCounter) inc() { 37 | // c.mutex.Lock() 38 | // defer c.mutex.Unlock() 39 | // c.Count += 1 40 | // } 41 | // 42 | // func (c *SessionCounter) dec() { 43 | // c.mutex.Lock() 44 | // defer c.mutex.Unlock() 45 | // c.Count -= 1 46 | // } 47 | // 48 | // func (c *SessionCounter) count() uint { 49 | // c.mutex.Lock() 50 | // defer c.mutex.Unlock() 51 | // return c.Count 52 | // } 53 | // 54 | // func getOriginalDst(conn net.Conn) (*net.TCPAddr, error) { 55 | // if runtime.GOOS != "linux" { 56 | // return nil, errors.New("'getOriginalDst' is only supported in Linux.") 57 | // } 58 | // 59 | // var file *os.File 60 | // var err error 61 | // 62 | // if _, ok := conn.(*net.TCPConn); ok { 63 | // file, err = conn.(*net.TCPConn).File() 64 | // if err != nil { 65 | // return nil, err 66 | // } 67 | // } else if _, ok := conn.(*tls.Conn); ok { 68 | // file, err = conn.(*tls.Conn).File() 69 | // if err != nil { 70 | // return nil, err 71 | // } 72 | // } else { 73 | // return nil, errors.New("Unknown Conn instance.") 74 | // } 75 | // 76 | // defer file.Close() 77 | // 78 | // // Set the file to non-blocking mode not to disable SetDeadline method. 79 | // fd := int(file.Fd()) 80 | // err = syscall.SetNonblock(fd, true) 81 | // if err != nil { 82 | // return nil, err 83 | // } 84 | // 85 | // origDstRaw, err := syscall.GetsockoptIPv6Mreq(fd, syscall.IPPROTO_IP, SO_ORIGINAL_DST) 86 | // if err != nil { 87 | // return nil, err 88 | // } 89 | // 90 | // ar := origDstRaw.Multiaddr[4:8] 91 | // pr := origDstRaw.Multiaddr[2:4] 92 | // 93 | // origDst := &net.TCPAddr{} 94 | // origDst.IP = net.IPv4(ar[0], ar[1], ar[2], ar[3]) 95 | // origDst.Port = (int(pr[0]) << 8) + int(pr[1]) 96 | // 97 | // return origDst, nil 98 | // } 99 | // 100 | // func HandleRequest(conn net.Conn, writer *RotWriter, timeout int) { 101 | // counter.inc() 102 | // defer counter.dec() 103 | // defer conn.Close() 104 | // 105 | // var err error 106 | // 107 | // var src, dst *net.TCPAddr 108 | // src = conn.RemoteAddr().(*net.TCPAddr) 109 | // 110 | // if runtime.GOOS == "linux" { 111 | // dst, err = getOriginalDst(conn) 112 | // if err != nil { 113 | // log.Printf("Failed to get original dst: %s", err) 114 | // } 115 | // } 116 | // 117 | // if dst == nil { 118 | // dst = conn.LocalAddr().(*net.TCPAddr) 119 | // } 120 | // 121 | // flow := NewTCPFlow(src, dst) 122 | // session := NewTCPSession(flow) 123 | // 124 | // log.Printf("Established: %s (#Sessions: %d)\n", session, counter.count()) 125 | // 126 | // var length uint 127 | // 128 | // buf := make([]byte, 2048) 129 | // 130 | // for { 131 | // conn.SetDeadline(time.Now().Add(time.Duration(timeout) * time.Second)) 132 | // 133 | // length, err := conn.Read(buf) 134 | // if err != nil { 135 | // break 136 | // } 137 | // 138 | // data := make([]byte, length) 139 | // copy(data, buf[:length]) 140 | // 141 | // session.AddPayload(data) 142 | // 143 | // log.Printf("Received: %s: %q (%d bytes)\n", session, buf[:length], length) 144 | // } 145 | // 146 | // if writer != nil { 147 | // outputJson, err := json.Marshal(session) 148 | // if err == nil { 149 | // log.Printf("Wrote: %s", session) 150 | // writer.Write(outputJson) 151 | // } else { 152 | // log.Printf("Failed to encode data as json: %s\n", err) 153 | // } 154 | // } 155 | // 156 | // if length == 0 { 157 | // log.Printf("Closed: %s (#Sessions: %d)\n", session, counter.count()) 158 | // } else { 159 | // log.Printf("Aborted: %s %s (#Sessions: %d)\n", session, err, counter.count()) 160 | // } 161 | // } 162 | -------------------------------------------------------------------------------- /tcppc/session.go: -------------------------------------------------------------------------------- 1 | package tcppc 2 | 3 | import ( 4 | "fmt" 5 | "github.com/jehiah/go-strftime" 6 | "net" 7 | "time" 8 | ) 9 | 10 | const ( 11 | TIME_FMT = "%Y-%m-%dT%H:%M:%S%z" 12 | ) 13 | 14 | func formatTimeStr(t *time.Time) string { 15 | return strftime.Format(TIME_FMT, *t) 16 | } 17 | 18 | type Flow struct { 19 | Proto string `json:"proto"` 20 | Src net.IP `json:"src"` 21 | Sport int `json:"sport"` 22 | Dst net.IP `json:"dst"` 23 | Dport int `json:"dport"` 24 | } 25 | 26 | func NewTCPFlow(src, dst *net.TCPAddr) *Flow { 27 | return &Flow{"tcp", src.IP, src.Port, dst.IP, dst.Port} 28 | } 29 | 30 | func NewTLSFlow(src, dst *net.TCPAddr) *Flow { 31 | return &Flow{"tls", src.IP, src.Port, dst.IP, dst.Port} 32 | } 33 | 34 | func NewUDPFlow(src, dst *net.UDPAddr) *Flow { 35 | return &Flow{"udp", src.IP, src.Port, dst.IP, dst.Port} 36 | } 37 | 38 | func (f *Flow) String() string { 39 | return fmt.Sprintf("Flow: %s %s:%d <-> %s:%d", f.Proto, f.Src.String(), f.Sport, f.Dst.String(), f.Dport) 40 | } 41 | 42 | type Payload struct { 43 | Index uint `json:"index"` 44 | Timestamp time.Time `json:"timestamp"` 45 | Data []byte `json:"data"` 46 | } 47 | 48 | func NewPayload(index uint, timestamp time.Time, data []byte) *Payload { 49 | return &Payload{index, timestamp, data} 50 | } 51 | 52 | func (p *Payload) String() string { 53 | return fmt.Sprintf("Payload %d: %s: %v", p.Index, formatTimeStr(&p.Timestamp), p.Data) 54 | } 55 | 56 | type Session struct { 57 | Timestamp time.Time `json:"timestamp"` 58 | Flow *Flow `json:"flow"` 59 | Payloads []*Payload `json:"payloads"` 60 | } 61 | 62 | func NewSession(flow *Flow) *Session { 63 | return &Session{Timestamp: time.Now(), Flow: flow} 64 | } 65 | 66 | func (s *Session) String() string { 67 | return fmt.Sprintf("Session: %s: %s (%d payloads)", formatTimeStr(&s.Timestamp), s.Flow, len(s.Payloads)) 68 | } 69 | 70 | func (s *Session) AddPayload(data []byte) *Payload { 71 | index := uint(len(s.Payloads)) 72 | ts := time.Now() 73 | 74 | payload := NewPayload(index, ts, data) 75 | s.Payloads = append(s.Payloads, payload) 76 | 77 | return payload 78 | } 79 | -------------------------------------------------------------------------------- /tcppc/tcp.go: -------------------------------------------------------------------------------- 1 | package tcppc 2 | 3 | import ( 4 | "encoding/json" 5 | "log" 6 | "net" 7 | "syscall" 8 | "time" 9 | ) 10 | 11 | func HandleTCPSession(conn *net.TCPConn, writer *RotWriter, timeout int) { 12 | defer conn.Close() 13 | defer counter.dec() 14 | counter.inc() 15 | 16 | var src, dst *net.TCPAddr 17 | src = conn.RemoteAddr().(*net.TCPAddr) 18 | dst = conn.LocalAddr().(*net.TCPAddr) 19 | 20 | flow := NewTCPFlow(src, dst) 21 | session := NewSession(flow) 22 | 23 | log.Printf("TCP: Established: %s (#Sessions: %d)\n", session, counter.count()) 24 | 25 | var length uint 26 | var err error 27 | 28 | buf := make([]byte, 4096) 29 | 30 | for { 31 | conn.SetDeadline(time.Now().Add(time.Duration(timeout) * time.Second)) 32 | 33 | length, err := conn.Read(buf) 34 | if err != nil { 35 | break 36 | } 37 | 38 | data := make([]byte, length) 39 | copy(data, buf[:length]) 40 | 41 | session.AddPayload(data) 42 | 43 | log.Printf("TCP: Received: %s: %q (%d bytes)\n", session, buf[:length], length) 44 | } 45 | 46 | if writer != nil { 47 | outputJson, err := json.Marshal(session) 48 | if err == nil { 49 | log.Printf("Wrote data: %s\n", session) 50 | writer.Write(outputJson) 51 | } else { 52 | log.Printf("Failed to encode data as json: %s\n", err) 53 | } 54 | } 55 | 56 | if length == 0 { 57 | log.Printf("Closed: %s (#Sessions: %d)\n", session, counter.count()) 58 | } else { 59 | log.Printf("Aborted: %s %s (#Sessions: %d)\n", session, err, counter.count()) 60 | } 61 | } 62 | 63 | func StartTCPServer(host string, port int, writer *RotWriter, timeout int) { 64 | log.Printf("Server Mode: TCP\n") 65 | log.Printf("Listen: %s:%d\n", host, port) 66 | 67 | addr := &net.TCPAddr{ 68 | IP: net.ParseIP(host), 69 | Port: port, 70 | } 71 | 72 | ln, err := net.ListenTCP("tcp", addr) 73 | if err != nil { 74 | log.Fatalf("Failed to listen TCP socket: %s\n", err) 75 | } 76 | defer ln.Close() 77 | 78 | file, err := ln.File() 79 | if err != nil { 80 | log.Fatalf("Failed to get a file descriptor of the listener: %s\n", err) 81 | } 82 | defer file.Close() 83 | 84 | fd := int(file.Fd()) 85 | if err := syscall.SetsockoptInt(fd, syscall.SOL_IP, syscall.IP_TRANSPARENT, 1); err != nil { 86 | log.Fatalf("Failed to set socket option (IP_TRANSPARENT): %s\n", err) 87 | } 88 | if err := syscall.SetsockoptInt(fd, syscall.SOL_IP, syscall.IP_RECVORIGDSTADDR, 1); err != nil { 89 | log.Fatalf("Failed to set socket option (IP_RECVORIGDSTADDR): %s\n", err) 90 | } 91 | 92 | log.Printf("Start TCP server.\n") 93 | 94 | for { 95 | conn, err := ln.AcceptTCP() 96 | if err != nil { 97 | log.Fatalf("Failed to accept a new connection: %s\n", err) 98 | } 99 | 100 | go HandleTCPSession(conn, writer, timeout) 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /tcppc/tls.go: -------------------------------------------------------------------------------- 1 | package tcppc 2 | 3 | import ( 4 | "crypto/tls" 5 | "encoding/json" 6 | "log" 7 | "net" 8 | "syscall" 9 | "time" 10 | ) 11 | 12 | func HandleTLSSession(conn *tls.Conn, writer *RotWriter, timeout int) { 13 | defer conn.Close() 14 | defer counter.dec() 15 | counter.inc() 16 | 17 | var src, dst *net.TCPAddr 18 | src = conn.RemoteAddr().(*net.TCPAddr) 19 | dst = conn.LocalAddr().(*net.TCPAddr) 20 | 21 | flow := NewTLSFlow(src, dst) 22 | session := NewSession(flow) 23 | 24 | log.Printf("TLS: Established: %s (#Sessions: %d)\n", session, counter.count()) 25 | 26 | var length uint 27 | var err error 28 | 29 | buf := make([]byte, 4096) 30 | 31 | for { 32 | conn.SetDeadline(time.Now().Add(time.Duration(timeout) * time.Second)) 33 | 34 | length, err := conn.Read(buf) 35 | if err != nil { 36 | break 37 | } 38 | 39 | data := make([]byte, length) 40 | copy(data, buf[:length]) 41 | 42 | session.AddPayload(data) 43 | 44 | log.Printf("TLS: Received: %s: %q (%d bytes)\n", session, buf[:length], length) 45 | } 46 | 47 | if writer != nil { 48 | outputJson, err := json.Marshal(session) 49 | if err == nil { 50 | log.Printf("Wrote data: %s\n", session) 51 | writer.Write(outputJson) 52 | } else { 53 | log.Printf("Failed to encode data as json: %s\n", err) 54 | } 55 | } 56 | 57 | if length == 0 { 58 | log.Printf("Closed: %s (#Sessions: %d)\n", session, counter.count()) 59 | } else { 60 | log.Printf("Aborted: %s %s (#Sessions: %d)\n", session, err, counter.count()) 61 | } 62 | } 63 | 64 | func StartTLSServer(host string, port int, config *tls.Config, writer *RotWriter, timeout int) { 65 | log.Printf("Server Mode: TLS\n") 66 | log.Printf("Listen: %s:%d\n", host, port) 67 | 68 | addr := &net.TCPAddr{ 69 | IP: net.ParseIP(host), 70 | Port: port, 71 | } 72 | 73 | tcpLn, err := net.ListenTCP("tcp", addr) 74 | if err != nil { 75 | log.Fatalf("Failed to listen TCP socket: %s\n", err) 76 | } 77 | defer tcpLn.Close() 78 | 79 | file, err := tcpLn.File() 80 | if err != nil { 81 | log.Fatalf("Failed to get a file descriptor of the listener: %s\n", err) 82 | } 83 | defer file.Close() 84 | 85 | fd := int(file.Fd()) 86 | if err := syscall.SetsockoptInt(fd, syscall.SOL_IP, syscall.IP_TRANSPARENT, 1); err != nil { 87 | log.Fatalf("Failed to set socket option (IP_TRANSPARENT): %s\n", err) 88 | } 89 | if err := syscall.SetsockoptInt(fd, syscall.SOL_IP, syscall.IP_RECVORIGDSTADDR, 1); err != nil { 90 | log.Fatalf("Failed to set socket option (IP_RECVORIGDSTADDR): %s\n", err) 91 | } 92 | 93 | ln := tls.NewListener(tcpLn, config) 94 | 95 | log.Printf("Start TLS server.\n") 96 | 97 | for { 98 | conn, err := ln.Accept() 99 | if err != nil { 100 | log.Fatalf("Failed to accept a new connection: %s\n", err) 101 | } 102 | 103 | go HandleTLSSession(conn.(*tls.Conn), writer, timeout) 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /tcppc/udp.go: -------------------------------------------------------------------------------- 1 | package tcppc 2 | 3 | import ( 4 | "bytes" 5 | "encoding/binary" 6 | "encoding/json" 7 | "errors" 8 | "log" 9 | "net" 10 | "syscall" 11 | "unsafe" 12 | ) 13 | 14 | func getOrigDst(oob []byte, oobn int) (*net.UDPAddr, error) { 15 | msgs, err := syscall.ParseSocketControlMessage(oob[:oobn]) 16 | if err != nil { 17 | return nil, err 18 | } 19 | 20 | var origDst *net.UDPAddr 21 | 22 | for _, msg := range msgs { 23 | if msg.Header.Level == syscall.SOL_IP && msg.Header.Type == syscall.IP_RECVORIGDSTADDR { 24 | origDstRaw := &syscall.RawSockaddrInet4{} 25 | if err := binary.Read(bytes.NewReader(msg.Data), binary.LittleEndian, origDstRaw); err != nil { 26 | return nil, err 27 | } 28 | 29 | switch origDstRaw.Family { 30 | case syscall.AF_INET: 31 | pp := (*syscall.RawSockaddrInet4)(unsafe.Pointer(origDstRaw)) 32 | p := (*[2]byte)(unsafe.Pointer(&pp.Port)) 33 | 34 | origDst = &net.UDPAddr{ 35 | IP: net.IPv4(pp.Addr[0], pp.Addr[1], pp.Addr[2], pp.Addr[3]), 36 | Port: int(p[0])<<8 + int(p[1]), 37 | } 38 | 39 | default: 40 | return nil, errors.New("Unsupported network family.") 41 | } 42 | } 43 | } 44 | 45 | return origDst, err 46 | } 47 | 48 | func HandleUDPSession(src, dst *net.UDPAddr, buf []byte, length int, writer *RotWriter) { 49 | flow := NewUDPFlow(src, dst) 50 | session := NewSession(flow) 51 | 52 | data := make([]byte, length) 53 | copy(data, buf[:length]) 54 | 55 | session.AddPayload(data) 56 | 57 | log.Printf("UDP: Received: %s: %q (%d bytes)\n", session, buf[:length], length) 58 | 59 | if writer != nil { 60 | outputJson, err := json.Marshal(session) 61 | if err == nil { 62 | log.Printf("Wrote data: %s", session) 63 | writer.Write(outputJson) 64 | } else { 65 | log.Printf("Failed to encode data as json: %s\n", err) 66 | } 67 | } 68 | } 69 | 70 | func StartUDPServer(host string, port int, writer *RotWriter) { 71 | log.Printf("Server Mode: UDP\n") 72 | log.Printf("Listen: %s:%d\n", host, port) 73 | 74 | addr := &net.UDPAddr{ 75 | IP: net.ParseIP(host), 76 | Port: int(port), 77 | } 78 | 79 | ln, err := net.ListenUDP("udp", addr) 80 | if err != nil { 81 | log.Fatalf("Failed to listen UDP socket: %s\n", err) 82 | } 83 | defer ln.Close() 84 | 85 | file, err := ln.File() 86 | if err != nil { 87 | log.Fatalf("Failed to get a file descriptor of the listener: %s", err) 88 | } 89 | defer file.Close() 90 | 91 | fd := int(file.Fd()) 92 | if err := syscall.SetsockoptInt(fd, syscall.SOL_IP, syscall.IP_TRANSPARENT, 1); err != nil { 93 | log.Fatalf("Failed to set socket option (IP_TRANSPARENT): %s\n", err) 94 | } 95 | if err := syscall.SetsockoptInt(fd, syscall.SOL_IP, syscall.IP_RECVORIGDSTADDR, 1); err != nil { 96 | log.Fatalf("Failed to set socket option (IP_RECVORIGDSTADDR): %s\n", err) 97 | } 98 | 99 | log.Printf("Start UDP server.\n") 100 | 101 | for { 102 | buf := make([]byte, 2048) 103 | oob := make([]byte, 1024) 104 | 105 | length, oobn, _, src, err := ln.ReadMsgUDP(buf, oob) 106 | if err != nil { 107 | log.Printf("Failed to read UDP message: %s\n", err) 108 | continue 109 | } 110 | 111 | origDst, err := getOrigDst(oob, oobn) 112 | if err != nil { 113 | log.Printf("Failed to get the original destination: %s\n", err) 114 | continue 115 | } 116 | 117 | go HandleUDPSession(src, origDst, buf, length, writer) 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /tcppc/writer.go: -------------------------------------------------------------------------------- 1 | package tcppc 2 | 3 | import ( 4 | "github.com/jehiah/go-strftime" 5 | "log" 6 | "os" 7 | "path/filepath" 8 | "sync" 9 | "time" 10 | ) 11 | 12 | func fileExists(filename string) bool { 13 | _, err := os.Stat(filename) 14 | return err == nil 15 | } 16 | 17 | type RotWriter struct { 18 | // Filename format w/ time indicators of strftime. 19 | FileNameFmt string 20 | // Rotation interval in second. 21 | RotInt int64 22 | // Rotation interval offset in second. 23 | RotOffset int64 24 | // Location used as timezone in FileNameFmt. 25 | Location *time.Location 26 | // Current file object. 27 | file *os.File 28 | // Last rotation time. 29 | lstRotTime int64 30 | // True if this writer is closed. 31 | // This flag is used to make the update goroutine exit. 32 | closed bool 33 | // Number of session data written to file. 34 | numSessions int 35 | // Mutex object for exclusive control of writing data to file. 36 | mutex sync.RWMutex 37 | } 38 | 39 | func NewWriter(fileNameFmt string, rotInt, rotOffset int, loc *time.Location) *RotWriter { 40 | w := &RotWriter{ 41 | FileNameFmt: fileNameFmt, 42 | RotInt: int64(rotInt), 43 | RotOffset: int64(rotOffset), 44 | Location: loc, 45 | file: nil, 46 | lstRotTime: 0, 47 | closed: false, 48 | } 49 | 50 | go func() { 51 | for { 52 | if w.closed { 53 | break 54 | } 55 | 56 | w.update() 57 | time.Sleep(100 * time.Millisecond) 58 | } 59 | }() 60 | 61 | return w 62 | } 63 | 64 | func (w *RotWriter) findFileName(ts int64) string { 65 | // Convert unix time to native time. 66 | tmpTime := time.Unix(ts, 0).In(w.Location) 67 | 68 | // Fill format of date and time in FileNameFmt. 69 | fileNameFmt := w.FileNameFmt 70 | fileName := strftime.Format(fileNameFmt, tmpTime) 71 | 72 | return fileName 73 | } 74 | 75 | func (w *RotWriter) update() { 76 | w.mutex.Lock() 77 | defer w.mutex.Unlock() 78 | 79 | curTime := time.Now().Unix() 80 | 81 | if curTime > w.lstRotTime && w.RotInt > 0 && (curTime%w.RotInt) == w.RotOffset { 82 | if w.file != nil { 83 | w.file.Close() 84 | w.file = nil 85 | 86 | log.Printf("Wrote %d session data.\n", w.numSessions) 87 | } 88 | } 89 | 90 | if w.file == nil { 91 | fileName := w.findFileName(curTime) 92 | dirName := filepath.Dir(fileName) 93 | 94 | // Create directories if not exists. 95 | if !fileExists(dirName) { 96 | err := os.MkdirAll(dirName, 0755) 97 | if err == nil { 98 | log.Printf("Create directories: %s\n", dirName) 99 | } else { 100 | log.Fatalf("Failed to create directories: %s (%s)", dirName, err) 101 | } 102 | } 103 | 104 | file, err := os.OpenFile(fileName, os.O_RDWR|os.O_CREATE|os.O_APPEND, 0644) 105 | if err == nil { 106 | log.Printf("Created a session file: %s\n", fileName) 107 | } else { 108 | log.Fatalf("Failed to create a session file: %s (%s)\n", fileName, err) 109 | } 110 | 111 | w.lstRotTime = curTime 112 | w.numSessions = 0 113 | w.file = file 114 | } 115 | } 116 | 117 | func (w *RotWriter) Write(data []byte) (n int, err error) { 118 | w.mutex.Lock() 119 | defer w.mutex.Unlock() 120 | 121 | w.numSessions += 1 122 | 123 | // Write data to file with '\n'. 124 | return w.file.Write(append(data, 0x0a)) 125 | } 126 | 127 | func (w *RotWriter) Close() error { 128 | w.mutex.Lock() 129 | defer w.mutex.Unlock() 130 | 131 | w.closed = true 132 | 133 | return w.file.Close() 134 | } 135 | --------------------------------------------------------------------------------